blob: 85642e802a4983e3f6c5f61d96dda376b126ccd0 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package okta
import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/helper/testhelpers"
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/helper/policyutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/okta/okta-sdk-golang/v2/okta"
"github.com/okta/okta-sdk-golang/v2/okta/query"
"github.com/stretchr/testify/require"
)
// To run this test, set the following env variables:
// VAULT_ACC=1
// OKTA_ORG=dev-219337
// OKTA_API_TOKEN=<generate via web UI, see Confluence for login details>
// OKTA_USERNAME=test3@example.com
// OKTA_PASSWORD=<find in 1password>
//
// You will need to install the Okta client app on your mobile device and
// setup MFA in order to use the Okta web UI. This test does not exercise
// MFA however (which is an enterprise feature), and therefore the test
// user in OKTA_USERNAME should not be configured with it. Currently
// test3@example.com is not a member of testgroup, which is the group with
// the profile that requires MFA.
func TestBackend_Config(t *testing.T) {
if os.Getenv("VAULT_ACC") == "" {
t.SkipNow()
}
// Ensure each cred is populated.
credNames := []string{
"OKTA_USERNAME",
"OKTA_PASSWORD",
"OKTA_API_TOKEN",
}
testhelpers.SkipUnlessEnvVarsSet(t, credNames)
defaultLeaseTTLVal := time.Hour * 12
maxLeaseTTLVal := time.Hour * 24
b, err := Factory(context.Background(), &logical.BackendConfig{
Logger: logging.NewVaultLogger(log.Trace),
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: defaultLeaseTTLVal,
MaxLeaseTTLVal: maxLeaseTTLVal,
},
})
if err != nil {
t.Fatalf("Unable to create backend: %s", err)
}
username := os.Getenv("OKTA_USERNAME")
password := os.Getenv("OKTA_PASSWORD")
token := os.Getenv("OKTA_API_TOKEN")
groupIDs := createOktaGroups(t, username, token, os.Getenv("OKTA_ORG"))
defer deleteOktaGroups(t, token, os.Getenv("OKTA_ORG"), groupIDs)
configData := map[string]interface{}{
"org_name": os.Getenv("OKTA_ORG"),
"base_url": "oktapreview.com",
}
updatedDuration := time.Hour * 1
configDataToken := map[string]interface{}{
"api_token": token,
"token_ttl": "1h",
}
logicaltest.Test(t, logicaltest.TestCase{
AcceptanceTest: true,
PreCheck: func() { testAccPreCheck(t) },
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testConfigCreate(t, configData),
// 2. Login with bad password, expect failure (E0000004=okta auth failure).
testLoginWrite(t, username, "wrong", "E0000004", 0, nil),
// 3. Make our user belong to two groups and have one user-specific policy.
testAccUserGroups(t, username, "local_grouP,lOcal_group2", []string{"user_policy"}),
// 4. Create the group local_group, assign it a single policy.
testAccGroups(t, "local_groUp", "loCal_group_policy"),
// 5. Login with good password, expect user to have their user-specific
// policy and the policy of the one valid group they belong to.
testLoginWrite(t, username, password, "", defaultLeaseTTLVal, []string{"local_group_policy", "user_policy"}),
// 6. Create the group everyone, assign it two policies. This is a
// magic group name in okta that always exists and which every
// user automatically belongs to.
testAccGroups(t, "everyoNe", "everyone_grouP_policy,eveRy_group_policy2"),
// 7. Login as before, expect same result
testLoginWrite(t, username, password, "", defaultLeaseTTLVal, []string{"local_group_policy", "user_policy"}),
// 8. Add API token so we can lookup groups
testConfigUpdate(t, configDataToken),
testConfigRead(t, token, configData),
// 10. Login should now lookup okta groups; since all okta users are
// in the "everyone" group, that should be returned; since we
// defined policies attached to the everyone group, we should now
// see those policies attached to returned vault token.
testLoginWrite(t, username, password, "", updatedDuration, []string{"everyone_group_policy", "every_group_policy2", "local_group_policy", "user_policy"}),
testAccGroups(t, "locAl_group2", "testgroup_group_policy"),
testLoginWrite(t, username, password, "", updatedDuration, []string{"everyone_group_policy", "every_group_policy2", "local_group_policy", "testgroup_group_policy", "user_policy"}),
},
})
}
func createOktaGroups(t *testing.T, username string, token string, org string) []string {
orgURL := "https://" + org + "." + previewBaseURL
ctx, client, err := okta.NewClient(context.Background(), okta.WithOrgUrl(orgURL), okta.WithToken(token))
require.Nil(t, err)
users, _, err := client.User.ListUsers(ctx, &query.Params{
Q: username,
})
require.Nil(t, err)
require.Len(t, users, 1)
userID := users[0].Id
var groupIDs []string
// Verify that login's call to list the groups of the user logging in will page
// through multiple result sets; note here
// https://developer.okta.com/docs/reference/api/groups/#list-groups-with-defaults
// that "If you don't specify a value for limit and don't specify a query,
// only 200 results are returned for most orgs."
for i := 0; i < 201; i++ {
name := fmt.Sprintf("TestGroup%d", i)
groups, _, err := client.Group.ListGroups(ctx, &query.Params{
Q: name,
})
require.Nil(t, err)
var groupID string
if len(groups) == 0 {
group, _, err := client.Group.CreateGroup(ctx, okta.Group{
Profile: &okta.GroupProfile{
Name: fmt.Sprintf("TestGroup%d", i),
},
})
require.Nil(t, err)
groupID = group.Id
} else {
groupID = groups[0].Id
}
groupIDs = append(groupIDs, groupID)
_, err = client.Group.AddUserToGroup(ctx, groupID, userID)
require.Nil(t, err)
}
return groupIDs
}
func deleteOktaGroups(t *testing.T, token string, org string, groupIDs []string) {
orgURL := "https://" + org + "." + previewBaseURL
ctx, client, err := okta.NewClient(context.Background(), okta.WithOrgUrl(orgURL), okta.WithToken(token))
require.Nil(t, err)
for _, groupID := range groupIDs {
_, err := client.Group.DeleteGroup(ctx, groupID)
require.Nil(t, err)
}
}
func testLoginWrite(t *testing.T, username, password, reason string, expectedTTL time.Duration, policies []string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login/" + username,
ErrorOk: true,
Data: map[string]interface{}{
"password": password,
},
Check: func(resp *logical.Response) error {
if resp.IsError() {
if reason == "" || !strings.Contains(resp.Error().Error(), reason) {
return resp.Error()
}
} else if reason != "" {
return fmt.Errorf("expected error containing %q, got no error", reason)
}
if resp.Auth != nil {
if !policyutil.EquivalentPolicies(resp.Auth.Policies, policies) {
return fmt.Errorf("policy mismatch expected %v but got %v", policies, resp.Auth.Policies)
}
actualTTL := resp.Auth.LeaseOptions.TTL
if actualTTL != expectedTTL {
return fmt.Errorf("TTL mismatch expected %v but got %v", expectedTTL, actualTTL)
}
}
return nil
},
}
}
func testConfigCreate(t *testing.T, d map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.CreateOperation,
Path: "config",
Data: d,
}
}
func testConfigUpdate(t *testing.T, d map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config",
Data: d,
}
}
func testConfigRead(t *testing.T, token string, d map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "config",
Check: func(resp *logical.Response) error {
if resp.IsError() {
return resp.Error()
}
if resp.Data["org_name"] != d["org_name"] {
return fmt.Errorf("org mismatch expected %s but got %s", d["organization"], resp.Data["Org"])
}
if resp.Data["base_url"] != d["base_url"] {
return fmt.Errorf("BaseURL mismatch expected %s but got %s", d["base_url"], resp.Data["BaseURL"])
}
for _, value := range resp.Data {
if value == token {
return fmt.Errorf("token should not be returned on a read request")
}
}
return nil
},
}
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("OKTA_USERNAME"); v == "" {
t.Fatal("OKTA_USERNAME must be set for acceptance tests")
}
if v := os.Getenv("OKTA_PASSWORD"); v == "" {
t.Fatal("OKTA_PASSWORD must be set for acceptance tests")
}
if v := os.Getenv("OKTA_ORG"); v == "" {
t.Fatal("OKTA_ORG must be set for acceptance tests")
}
if v := os.Getenv("OKTA_API_TOKEN"); v == "" {
t.Fatal("OKTA_API_TOKEN must be set for acceptance tests")
}
}
func testAccUserGroups(t *testing.T, user string, groups interface{}, policies interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "users/" + user,
Data: map[string]interface{}{
"groups": groups,
"policies": policies,
},
}
}
func testAccGroups(t *testing.T, group string, policies interface{}) logicaltest.TestStep {
t.Logf("[testAccGroups] - Registering group %s, policy %s", group, policies)
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "groups/" + group,
Data: map[string]interface{}{
"policies": policies,
},
}
}
func testAccLogin(t *testing.T, user, password string, keys []string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login/" + user,
Data: map[string]interface{}{
"password": password,
},
Unauthenticated: true,
Check: logicaltest.TestCheckAuth(keys),
}
}