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

Add user_factor_question Resource #551

Merged
merged 6 commits into from
Jul 28, 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
18 changes: 18 additions & 0 deletions examples/okta_user_factor_question/basic.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
resource "okta_user" "test" {
first_name = "TestAcc"
last_name = "Jones"
login = "john_replace_with_uuid@ledzeppelin.com"
email = "john_replace_with_uuid@ledzeppelin.com"
}

resource "okta_factor" "test_factor" {
provider_id = "okta_question"
active = true
}

resource "okta_user_factor_question" "test" {
user_id = okta_user.test.id
key = "disliked_food"
answer = "meatball"
depends_on = [okta_factor.test_factor]
}
18 changes: 18 additions & 0 deletions examples/okta_user_factor_question/updated.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
resource "okta_user" "test" {
first_name = "TestAcc"
last_name = "Jones"
login = "john_replace_with_uuid@ledzeppelin.com"
email = "john_replace_with_uuid@ledzeppelin.com"
}

resource "okta_factor" "test_factor" {
provider_id = "okta_question"
active = true
}

resource "okta_user_factor_question" "test" {
user_id = okta_user.test.id
key = "name_of_first_plush_toy"
answer = "meatball"
depends_on = [okta_factor.test_factor]
}
2 changes: 2 additions & 0 deletions okta/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const (
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 @@ -233,6 +234,7 @@ func Provider() *schema.Provider {
userType: resourceUserType(),
userGroupMemberships: resourceUserGroupMemberships(),
userAdminRoles: resourceUserAdminRoles(),
userFactorQuestion: resourceUserFactorQuestion(),

// The day I realized I was naming stuff wrong :'-(
"okta_idp": deprecateIncorrectNaming(resourceIdpOidc(), idpOidc),
Expand Down
12 changes: 6 additions & 6 deletions okta/resource_okta_factor.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func resourceFactor() *schema.Resource {
}

func resourceFactorPut(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
factor, _, err := getSupplementFromMetadata(m).GetFactor(ctx, d.Get("provider_id").(string))
factor, _, err := getSupplementFromMetadata(m).GetOrgFactor(ctx, d.Get("provider_id").(string))
if err != nil {
return diag.Errorf("failed to find factor: %v", err)
}
Expand All @@ -74,7 +74,7 @@ func resourceFactorPut(ctx context.Context, d *schema.ResourceData, m interface{
}

func resourceFactorRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
factor, resp, err := getSupplementFromMetadata(m).GetFactor(ctx, d.Get("provider_id").(string))
factor, resp, err := getSupplementFromMetadata(m).GetOrgFactor(ctx, d.Get("provider_id").(string))
if err := suppressErrorOn404(resp, err); err != nil {
return diag.Errorf("failed to find factor: %v", err)
}
Expand All @@ -91,7 +91,7 @@ func resourceFactorDelete(ctx context.Context, d *schema.ResourceData, m interfa
if !d.Get("active").(bool) {
return nil
}
_, resp, err := getSupplementFromMetadata(m).DeactivateFactor(ctx, d.Id())
_, resp, err := getSupplementFromMetadata(m).DeactivateOrgFactor(ctx, d.Id())
// http.StatusBadRequest means that factor can not be deactivated
if resp != nil && resp.StatusCode == http.StatusBadRequest {
return nil
Expand All @@ -106,14 +106,14 @@ func activateFactor(ctx context.Context, d *schema.ResourceData, m interface{})
var err error
id := d.Get("provider_id").(string)
if d.Get("active").(bool) {
_, _, err = getSupplementFromMetadata(m).ActivateFactor(ctx, id)
_, _, err = getSupplementFromMetadata(m).ActivateOrgFactor(ctx, id)
} else {
_, _, err = getSupplementFromMetadata(m).DeactivateFactor(ctx, id)
_, _, err = getSupplementFromMetadata(m).DeactivateOrgFactor(ctx, id)
}
return err
}

func statusMismatch(d *schema.ResourceData, factor *sdk.Factor) bool {
func statusMismatch(d *schema.ResourceData, factor *sdk.OrgFactor) bool {
status := d.Get("active").(bool)

// I miss ternary operators
Expand Down
141 changes: 141 additions & 0 deletions okta/resource_okta_user_factor_question.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package okta

import (
"context"
"fmt"
"net/http"
"strings"

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

func resourceUserFactorQuestion() *schema.Resource {
return &schema.Resource{
CreateContext: resourceUserFactorQuestionCreate,
ReadContext: resourceUserFactorQuestionRead,
UpdateContext: resourceUserFactorQuestionUpdate,
DeleteContext: resourceUserFactorQuestionDelete,
Importer: createNestedResourceImporter([]string{"user_id", "id"}),
Description: "Resource to manage a question factor for a user",
Schema: map[string]*schema.Schema{
"user_id": {
Type: schema.TypeString,
Required: true,
Description: "ID of a Okta User",
ForceNew: true,
},
"key": {
Type: schema.TypeString,
Required: true,
Description: "Unique key for question",
},
"answer": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
ValidateDiagFunc: stringLenBetween(4, 1000),
Description: "User password security answer",
},
"text": {
Type: schema.TypeString,
Computed: true,
Description: "Display text for question",
},
"status": {
Type: schema.TypeString,
Computed: true,
Description: "User factor status.",
},
},
}
}

func resourceUserFactorQuestionCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
err := validateQuestionKey(ctx, d, m)
if err != nil {
return diag.FromErr(err)
}
sq := buildUserFactorQuestion(d)
_, err = getSupplementFromMetadata(m).EnrollUserFactor(ctx, d.Get("user_id").(string), sq, nil)
if err != nil {
return diag.Errorf("failed to enroll user question factor: %v", err)
}
d.SetId(sq.Id)
return resourceUserFactorQuestionRead(ctx, d, m)
}

func resourceUserFactorQuestionRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
var uf okta.SecurityQuestionUserFactor
resp, err := getSupplementFromMetadata(m).GetUserFactor(ctx, d.Get("user_id").(string), d.Id(), &uf)
if err := suppressErrorOn404(resp, err); err != nil {
return diag.Errorf("failed to get user question factor: %v", err)
}
if uf.Id == "" {
d.SetId("")
return nil
}
_ = d.Set("status", uf.Status)
_ = d.Set("key", uf.Profile.Question)
_ = d.Set("text", uf.Profile.QuestionText)
return nil
}

func resourceUserFactorQuestionUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
err := validateQuestionKey(ctx, d, m)
if err != nil {
return diag.FromErr(err)
}
sq := &sdk.SecurityQuestionUserFactor{
Profile: &okta.SecurityQuestionUserFactorProfile{
Answer: d.Get("answer").(string),
Question: d.Get("key").(string),
},
}
_, err = getSupplementFromMetadata(m).UpdateUserFactor(ctx, d.Get("user_id").(string), d.Id(), sq)
if err != nil {
return diag.Errorf("failed to update user question factor: %v", err)
}
return resourceUserFactorQuestionRead(ctx, d, m)
}

func resourceUserFactorQuestionDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
resp, err := getOktaClientFromMetadata(m).UserFactor.DeleteFactor(ctx, d.Get("user_id").(string), d.Id())
if err != nil {
// disabled factor can not be removed
if resp != nil && resp.StatusCode == http.StatusBadRequest {
return nil
}
return diag.Errorf("failed to delete user question factor: %v", err)
}
return nil
}

func buildUserFactorQuestion(d *schema.ResourceData) *sdk.SecurityQuestionUserFactor {
return &sdk.SecurityQuestionUserFactor{
FactorType: "question",
Provider: "OKTA",
Profile: &okta.SecurityQuestionUserFactorProfile{
Answer: d.Get("answer").(string),
Question: d.Get("key").(string),
},
}
}

func validateQuestionKey(ctx context.Context, d *schema.ResourceData, m interface{}) error {
sq, _, err := getOktaClientFromMetadata(m).UserFactor.ListSupportedSecurityQuestions(ctx, d.Get("user_id").(string))
if err != nil {
return fmt.Errorf("failed to list security question keys: %v", err)
}
keys := make([]string, len(sq))
for i := range sq {
if sq[i].Question == d.Get("key").(string) {
return nil
}
keys[i] = sq[i].Question
}
return fmt.Errorf("'%s' is missing from the available questions keys, please use one of [%s]",
d.Get("key").(string), strings.Join(keys, ","))
}
70 changes: 70 additions & 0 deletions okta/resource_okta_user_factor_question_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package okta

import (
"context"
"fmt"
"testing"

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

func TestAccOktaUserFactorQuestion_crud(t *testing.T) {
ri := acctest.RandInt()
mgr := newFixtureManager("okta_user_factor_question")
config := mgr.GetFixtures("basic.tf", ri, t)
updated := mgr.GetFixtures("updated.tf", ri, t)
resourceName := fmt.Sprintf("%s.test", userFactorQuestion)
resource.Test(
t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProvidersFactories,
CheckDestroy: createUserFactorCheckDestroy(userFactorQuestion),
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "key", "disliked_food"),
resource.TestCheckResourceAttr(resourceName, "answer", "meatball"),
resource.TestCheckResourceAttr(resourceName, "status", "ACTIVE"),
),
},
{
Config: updated,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "key", "name_of_first_plush_toy"),
resource.TestCheckResourceAttr(resourceName, "answer", "meatball"),
resource.TestCheckResourceAttr(resourceName, "status", "ACTIVE"),
),
},
},
})
}

func createUserFactorCheckDestroy(FactorType string) func(*terraform.State) error {
return func(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != FactorType {
continue
}
userID := rs.Primary.Attributes["user_id"]
ID := rs.Primary.ID
exists, err := doesUserFactorExistsUpstream(userID, ID)
if err != nil {
return err
}
if exists {
return fmt.Errorf("user factor still exists,userID: %s, user factor ID: %s", userID, ID)
}
}
return nil
}
}

func doesUserFactorExistsUpstream(userId string, factorId string) (bool, error) {
var uf *okta.SecurityQuestionUserFactor
_, resp, err := getOktaClientFromMetadata(testAccProvider.Meta()).UserFactor.GetFactor(context.Background(), userId, factorId, uf)
return doesResourceExist(resp, err)
}
26 changes: 13 additions & 13 deletions sdk/factor.go → sdk/org_factor.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import (
"github.com/okta/okta-sdk-golang/v2/okta"
)

type Factor struct {
type OrgFactor struct {
Id string `json:"id"`
Provider string `json:"provider"`
FactorType string `json:"factorType"`
Status string `json:"status"`
}

// Current available factors for MFA
// Current available org factors for MFA
const (
DuoFactor = "duo"
FidoU2fFactor = "fido_u2f"
Expand All @@ -34,32 +34,32 @@ const (
HotpFactor = "hotp"
)

// GetFactor gets a factor by ID.
func (m *ApiSupplement) GetFactor(ctx context.Context, id string) (*Factor, *okta.Response, error) {
// GetOrgFactor gets a factor by ID.
func (m *ApiSupplement) GetOrgFactor(ctx context.Context, id string) (*OrgFactor, *okta.Response, error) {
url := fmt.Sprintf("/api/v1/org/factors/%s", id)
req, err := m.RequestExecutor.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, nil, err
}
var factor Factor
var factor OrgFactor
resp, err := m.RequestExecutor.Do(ctx, req, &factor)
if err != nil {
return nil, resp, err
}
return &factor, resp, nil
}

// ActivateFactor allows multifactor authentication to use provided factor type
func (m *ApiSupplement) ActivateFactor(ctx context.Context, id string) (*Factor, *okta.Response, error) {
return m.lifecycleChangeFactor(ctx, id, "activate")
// ActivateOrgFactor allows multifactor authentication to use provided factor type
func (m *ApiSupplement) ActivateOrgFactor(ctx context.Context, id string) (*OrgFactor, *okta.Response, error) {
return m.lifecycleChangeOrgFactor(ctx, id, "activate")
}

// ActivateFactor denies multifactor authentication to use provided factor type
func (m *ApiSupplement) DeactivateFactor(ctx context.Context, id string) (*Factor, *okta.Response, error) {
return m.lifecycleChangeFactor(ctx, id, "deactivate")
// DeactivateOrgFactor denies multifactor authentication to use provided factor type
func (m *ApiSupplement) DeactivateOrgFactor(ctx context.Context, id string) (*OrgFactor, *okta.Response, error) {
return m.lifecycleChangeOrgFactor(ctx, id, "deactivate")
}

func (m *ApiSupplement) lifecycleChangeFactor(ctx context.Context, id, action string) (*Factor, *okta.Response, error) {
func (m *ApiSupplement) lifecycleChangeOrgFactor(ctx context.Context, id, action string) (*OrgFactor, *okta.Response, error) {
url := fmt.Sprintf("/api/v1/org/factors/%s/lifecycle/%s", id, action)
req, err := m.RequestExecutor.
WithAccept("application/json").
Expand All @@ -68,7 +68,7 @@ func (m *ApiSupplement) lifecycleChangeFactor(ctx context.Context, id, action st
if err != nil {
return nil, nil, err
}
var factor *Factor
var factor *OrgFactor
resp, err := m.RequestExecutor.Do(ctx, req, factor)
if err != nil {
return nil, resp, err
Expand Down
Loading