vault/command/agent/approle_end_to_end_test.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

806 lines
20 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package agent
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
"time"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
credAppRole "github.com/hashicorp/vault/builtin/credential/approle"
"github.com/hashicorp/vault/command/agentproxyshared/auth"
agentapprole "github.com/hashicorp/vault/command/agentproxyshared/auth/approle"
"github.com/hashicorp/vault/command/agentproxyshared/sink"
"github.com/hashicorp/vault/command/agentproxyshared/sink/file"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
)
func TestAppRoleEndToEnd(t *testing.T) {
t.Parallel()
testCases := []struct {
removeSecretIDFile bool
bindSecretID bool
secretIDLess bool
expectToken bool
}{
// default behaviour => token expected
{false, true, false, true},
{true, true, false, true},
//bindSecretID=false, wrong secret provided => token expected
//(vault ignores the supplied secret_id if bind_secret_id=false)
{false, false, false, true},
{true, false, false, true},
// bindSecretID=false, secret not provided => token expected
{false, false, true, true},
{true, false, true, true},
// bindSecretID=true, secret not provided => token not expected
{false, true, true, false},
{true, true, true, false},
}
for _, tc := range testCases {
secretFileAction := "preserve"
if tc.removeSecretIDFile {
secretFileAction = "remove"
}
tc := tc // capture range variable
t.Run(fmt.Sprintf("%s_secret_id_file bindSecretID=%v secretIDLess=%v expectToken=%v", secretFileAction, tc.bindSecretID, tc.secretIDLess, tc.expectToken), func(t *testing.T) {
t.Parallel()
testAppRoleEndToEnd(t, tc.removeSecretIDFile, tc.bindSecretID, tc.secretIDLess, tc.expectToken)
})
}
}
func testAppRoleEndToEnd(t *testing.T, removeSecretIDFile bool, bindSecretID bool, secretIDLess bool, expectToken bool) {
var err error
logger := logging.NewVaultLogger(log.Trace)
coreConfig := &vault.CoreConfig{
DisableMlock: true,
DisableCache: true,
CredentialBackends: map[string]logical.Factory{
"approle": credAppRole.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
cores := cluster.Cores
vault.TestWaitActive(t, cores[0].Core)
client := cores[0].Client
err = client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{
Type: "approle",
})
if err != nil {
t.Fatal(err)
}
_, err = client.Logical().Write("auth/approle/role/test1", addConstraints(!bindSecretID, map[string]interface{}{
"bind_secret_id": bindSecretID,
"token_ttl": "6s",
"token_max_ttl": "10s",
}))
logger.Trace("vault configured with", "bind_secret_id", bindSecretID)
if err != nil {
t.Fatal(err)
}
secret := ""
secretID1 := ""
secretID2 := ""
if bindSecretID {
resp, err := client.Logical().Write("auth/approle/role/test1/secret-id", nil)
if err != nil {
t.Fatal(err)
}
secretID1 = resp.Data["secret_id"].(string)
} else {
logger.Trace("skipped write to auth/approle/role/test1/secret-id")
}
resp, err := client.Logical().Read("auth/approle/role/test1/role-id")
if err != nil {
t.Fatal(err)
}
roleID1 := resp.Data["role_id"].(string)
_, err = client.Logical().Write("auth/approle/role/test2", addConstraints(!bindSecretID, map[string]interface{}{
"bind_secret_id": bindSecretID,
"token_ttl": "6s",
"token_max_ttl": "10s",
}))
if err != nil {
t.Fatal(err)
}
if bindSecretID {
resp, err = client.Logical().Write("auth/approle/role/test2/secret-id", nil)
if err != nil {
t.Fatal(err)
}
secretID2 = resp.Data["secret_id"].(string)
} else {
logger.Trace("skipped write to auth/approle/role/test2/secret-id")
}
resp, err = client.Logical().Read("auth/approle/role/test2/role-id")
if err != nil {
t.Fatal(err)
}
roleID2 := resp.Data["role_id"].(string)
rolef, err := ioutil.TempFile("", "auth.role-id.test.")
if err != nil {
t.Fatal(err)
}
role := rolef.Name()
rolef.Close() // WriteFile doesn't need it open
defer os.Remove(role)
t.Logf("input role_id_file_path: %s", role)
if bindSecretID {
secretf, err := ioutil.TempFile("", "auth.secret-id.test.")
if err != nil {
t.Fatal(err)
}
secret = secretf.Name()
secretf.Close()
defer os.Remove(secret)
t.Logf("input secret_id_file_path: %s", secret)
} else {
logger.Trace("skipped writing tempfile auth.secret-id.test.")
}
// We close these right away because we're just basically testing
// permissions and finding a usable file name
ouf, err := ioutil.TempFile("", "auth.tokensink.test.")
if err != nil {
t.Fatal(err)
}
out := ouf.Name()
ouf.Close()
os.Remove(out)
t.Logf("output: %s", out)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
secretFromAgent := secret
if secretIDLess {
secretFromAgent = ""
}
if !bindSecretID && !secretIDLess {
logger.Trace("agent is providing an invalid secret that should be ignored")
secretf, err := ioutil.TempFile("", "auth.secret-id.test.")
if err != nil {
t.Fatal(err)
}
secretFromAgent = secretf.Name()
secretf.Close()
defer os.Remove(secretFromAgent)
// if the token is empty, auth.approle would fail reporting the error
if err := ioutil.WriteFile(secretFromAgent, []byte("wrong-secret"), 0o600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote secret_id_file_path with wrong-secret", "path", secretFromAgent)
}
}
conf := map[string]interface{}{
"role_id_file_path": role,
"secret_id_file_path": secretFromAgent,
}
logger.Trace("agent configured with", "conf", conf)
if !removeSecretIDFile {
conf["remove_secret_id_file_after_reading"] = removeSecretIDFile
}
am, err := agentapprole.NewApproleAuthMethod(&auth.AuthConfig{
Logger: logger.Named("auth.approle"),
MountPath: "auth/approle",
Config: conf,
})
if err != nil {
t.Fatal(err)
}
ahConfig := &auth.AuthHandlerConfig{
Logger: logger.Named("auth.handler"),
Client: client,
}
ah := auth.NewAuthHandler(ahConfig)
errCh := make(chan error)
go func() {
errCh <- ah.Run(ctx, am)
}()
defer func() {
select {
case <-ctx.Done():
case err := <-errCh:
if err != nil {
t.Fatal(err)
}
}
}()
config := &sink.SinkConfig{
Logger: logger.Named("sink.file"),
Config: map[string]interface{}{
"path": out,
},
}
fs, err := file.NewFileSink(config)
if err != nil {
t.Fatal(err)
}
config.Sink = fs
ss := sink.NewSinkServer(&sink.SinkServerConfig{
Logger: logger.Named("sink.server"),
Client: client,
})
go func() {
errCh <- ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config})
}()
defer func() {
select {
case <-ctx.Done():
case err := <-errCh:
if err != nil {
t.Fatal(err)
}
}
}()
// This has to be after the other defers so it happens first. It allows
// successful test runs to immediately cancel all of the runner goroutines
// and unblock any of the blocking defer calls by the runner's DoneCh that
// comes before this and avoid successful tests from taking the entire
// timeout duration.
defer cancel()
// Check that no sink file exists
_, err = os.Lstat(out)
if err == nil {
t.Fatal("expected err")
}
if !os.IsNotExist(err) {
t.Fatal("expected notexist err")
}
if err := ioutil.WriteFile(role, []byte(roleID1), 0o600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote test role 1", "path", role)
}
if bindSecretID {
if err := ioutil.WriteFile(secret, []byte(secretID1), 0o600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote test secret 1", "path", secret)
}
} else {
logger.Trace("skipped writing test secret 1")
}
checkToken := func() string {
timeout := time.Now().Add(10 * time.Second)
for {
if time.Now().After(timeout) {
if expectToken {
t.Fatal("did not find a written token after timeout")
}
return ""
}
val, err := ioutil.ReadFile(out)
if err == nil {
os.Remove(out)
if len(val) == 0 {
t.Fatal("written token was empty")
}
if !secretIDLess {
_, err = os.Stat(secretFromAgent)
switch {
case removeSecretIDFile && err == nil:
t.Fatal("secret file exists but was supposed to be removed")
case !removeSecretIDFile && err != nil:
t.Fatal("secret ID file does not exist but was not supposed to be removed")
}
}
client.SetToken(string(val))
secret, err := client.Auth().Token().LookupSelf()
if err != nil {
t.Fatal(err)
}
return secret.Data["entity_id"].(string)
}
time.Sleep(250 * time.Millisecond)
}
}
origEntity := checkToken()
if !expectToken && origEntity != "" {
t.Fatal("did not expect a token to be written: " + origEntity)
}
if !expectToken && origEntity == "" {
logger.Trace("skipping entities comparison as we are not expecting tokens to be written")
return
}
// Make sure it gets renewed
timeout := time.Now().Add(4 * time.Second)
for {
if time.Now().After(timeout) {
break
}
secret, err := client.Auth().Token().LookupSelf()
if err != nil {
t.Fatal(err)
}
ttl, err := secret.Data["ttl"].(json.Number).Int64()
if err != nil {
t.Fatal(err)
}
if ttl > 6 {
t.Fatalf("unexpected ttl: %v", secret.Data["ttl"])
}
}
// Write new values
if err := ioutil.WriteFile(role, []byte(roleID2), 0o600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote test role 2", "path", role)
}
if bindSecretID {
if err := ioutil.WriteFile(secret, []byte(secretID2), 0o600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote test secret 2", "path", secret)
}
} else {
logger.Trace("skipped writing test secret 2")
}
newEntity := checkToken()
if newEntity == origEntity {
t.Fatal("found same entity")
}
timeout = time.Now().Add(4 * time.Second)
for {
if time.Now().After(timeout) {
break
}
secret, err := client.Auth().Token().LookupSelf()
if err != nil {
t.Fatal(err)
}
ttl, err := secret.Data["ttl"].(json.Number).Int64()
if err != nil {
t.Fatal(err)
}
if ttl > 6 {
t.Fatalf("unexpected ttl: %v", secret.Data["ttl"])
}
}
}
// TestAppRoleLongRoleName tests that the creation of an approle is a maximum of 4096 bytes
// Prior to VAULT-8518 being fixed, you were unable to delete an approle value longer than 1024 bytes
// due to a restriction put into place by PR #14746, to prevent unbounded HMAC creation.
func TestAppRoleLongRoleName(t *testing.T) {
approleName := strings.Repeat("a", 5000)
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"approle": credAppRole.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
cores := cluster.Cores
vault.TestWaitActive(t, cores[0].Core)
client := cores[0].Client
err := client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{
Type: "approle",
})
if err != nil {
t.Fatal(err)
}
_, err = client.Logical().Write(fmt.Sprintf("auth/approle/role/%s", approleName), map[string]interface{}{
"token_ttl": "6s",
"token_max_ttl": "10s",
})
if err != nil {
if !strings.Contains(err.Error(), "role_name is longer than maximum") {
t.Fatal(err)
}
}
}
func TestAppRoleWithWrapping(t *testing.T) {
testCases := []struct {
bindSecretID bool
secretIDLess bool
expectToken bool
}{
// default behaviour => token expected
{true, false, true},
//bindSecretID=false, wrong secret provided, wrapping_path provided => token not expected
//(wrapping token is not valid or does not exist)
{false, false, false},
// bindSecretID=false, no secret provided, wrapping_path provided but ignored => token expected
{false, true, true},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("bindSecretID=%v secretIDLess=%v expectToken=%v", tc.bindSecretID, tc.secretIDLess, tc.expectToken), func(t *testing.T) {
testAppRoleWithWrapping(t, tc.bindSecretID, tc.secretIDLess, tc.expectToken)
})
}
}
func testAppRoleWithWrapping(t *testing.T, bindSecretID bool, secretIDLess bool, expectToken bool) {
var err error
logger := logging.NewVaultLogger(log.Trace)
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"approle": credAppRole.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
cores := cluster.Cores
vault.TestWaitActive(t, cores[0].Core)
client := cores[0].Client
origToken := client.Token()
err = client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{
Type: "approle",
})
if err != nil {
t.Fatal(err)
}
_, err = client.Logical().Write("auth/approle/role/test1", addConstraints(!bindSecretID, map[string]interface{}{
"bind_secret_id": bindSecretID,
"token_ttl": "6s",
"token_max_ttl": "10s",
}))
if err != nil {
t.Fatal(err)
}
client.SetWrappingLookupFunc(func(operation, path string) string {
if path == "auth/approle/role/test1/secret-id" {
return "10s"
}
return ""
})
secret := ""
secretID1 := ""
if bindSecretID {
resp, err := client.Logical().Write("auth/approle/role/test1/secret-id", nil)
if err != nil {
t.Fatal(err)
}
secretID1 = resp.WrapInfo.Token
} else {
logger.Trace("skipped write to auth/approle/role/test1/secret-id")
}
resp, err := client.Logical().Read("auth/approle/role/test1/role-id")
if err != nil {
t.Fatal(err)
}
roleID1 := resp.Data["role_id"].(string)
rolef, err := ioutil.TempFile("", "auth.role-id.test.")
if err != nil {
t.Fatal(err)
}
role := rolef.Name()
rolef.Close() // WriteFile doesn't need it open
defer os.Remove(role)
t.Logf("input role_id_file_path: %s", role)
if bindSecretID {
secretf, err := ioutil.TempFile("", "auth.secret-id.test.")
if err != nil {
t.Fatal(err)
}
secret = secretf.Name()
secretf.Close()
defer os.Remove(secret)
t.Logf("input secret_id_file_path: %s", secret)
} else {
logger.Trace("skipped writing tempfile auth.secret-id.test.")
}
// We close these right away because we're just basically testing
// permissions and finding a usable file name
ouf, err := ioutil.TempFile("", "auth.tokensink.test.")
if err != nil {
t.Fatal(err)
}
out := ouf.Name()
ouf.Close()
os.Remove(out)
t.Logf("output: %s", out)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
secretFromAgent := secret
if secretIDLess {
secretFromAgent = ""
}
if !bindSecretID && !secretIDLess {
logger.Trace("agent is providing an invalid secret that should be ignored")
secretf, err := ioutil.TempFile("", "auth.secret-id.test.")
if err != nil {
t.Fatal(err)
}
secretFromAgent = secretf.Name()
secretf.Close()
defer os.Remove(secretFromAgent)
// if the token is empty, auth.approle would fail reporting the error
if err := ioutil.WriteFile(secretFromAgent, []byte("wrong-secret"), 0o600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote secret_id_file_path with wrong-secret", "path", secretFromAgent)
}
}
conf := map[string]interface{}{
"role_id_file_path": role,
"secret_id_file_path": secretFromAgent,
"secret_id_response_wrapping_path": "auth/approle/role/test1/secret-id",
"remove_secret_id_file_after_reading": true,
}
logger.Trace("agent configured with", "conf", conf)
am, err := agentapprole.NewApproleAuthMethod(&auth.AuthConfig{
Logger: logger.Named("auth.approle"),
MountPath: "auth/approle",
Config: conf,
})
if err != nil {
t.Fatal(err)
}
ahConfig := &auth.AuthHandlerConfig{
Logger: logger.Named("auth.handler"),
Client: client,
}
ah := auth.NewAuthHandler(ahConfig)
errCh := make(chan error)
go func() {
errCh <- ah.Run(ctx, am)
}()
defer func() {
select {
case <-ctx.Done():
case err := <-errCh:
if err != nil {
t.Fatal(err)
}
}
}()
config := &sink.SinkConfig{
Logger: logger.Named("sink.file"),
Config: map[string]interface{}{
"path": out,
},
}
fs, err := file.NewFileSink(config)
if err != nil {
t.Fatal(err)
}
config.Sink = fs
ss := sink.NewSinkServer(&sink.SinkServerConfig{
Logger: logger.Named("sink.server"),
Client: client,
})
go func() {
errCh <- ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config})
}()
defer func() {
select {
case <-ctx.Done():
case err := <-errCh:
if err != nil {
t.Fatal(err)
}
}
}()
// This has to be after the other defers so it happens first. It allows
// successful test runs to immediately cancel all of the runner goroutines
// and unblock any of the blocking defer calls by the runner's DoneCh that
// comes before this and avoid successful tests from taking the entire
// timeout duration.
defer cancel()
// Check that no sink file exists
_, err = os.Lstat(out)
if err == nil {
t.Fatal("expected err")
}
if !os.IsNotExist(err) {
t.Fatal("expected notexist err")
}
if err := ioutil.WriteFile(role, []byte(roleID1), 0o600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote test role 1", "path", role)
}
if bindSecretID {
logger.Trace("WRITING TO auth.secret-id.test.", "secret", secret, "secretID1", secretID1)
if err := ioutil.WriteFile(secret, []byte(secretID1), 0o600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote test secret 1", "path", secret)
}
} else {
logger.Trace("skipped writing test secret 1")
}
checkToken := func() string {
timeout := time.Now().Add(10 * time.Second)
for {
if time.Now().After(timeout) {
if expectToken {
t.Fatal("did not find a written token after timeout")
}
return ""
}
val, err := ioutil.ReadFile(out)
if err == nil {
os.Remove(out)
if len(val) == 0 {
t.Fatal("written token was empty")
}
if !secretIDLess {
if _, err := os.Stat(secret); err == nil {
t.Fatal("secret ID file does not exist but was not supposed to be removed")
}
}
client.SetToken(string(val))
secret, err := client.Auth().Token().LookupSelf()
if err != nil {
t.Fatal(err)
}
return secret.Data["entity_id"].(string)
}
time.Sleep(250 * time.Millisecond)
}
}
origEntity := checkToken()
logger.Trace("cheking token", "origEntity", origEntity)
if !expectToken && origEntity != "" {
t.Fatal("did not expect a token to be written: " + origEntity)
}
if !expectToken && origEntity == "" {
logger.Trace("skipping entities comparison as we are not expecting tokens to be written")
return
}
// Make sure it gets renewed
timeout := time.Now().Add(4 * time.Second)
for {
if time.Now().After(timeout) {
break
}
secret, err := client.Auth().Token().LookupSelf()
if err != nil {
t.Fatal(err)
}
ttl, err := secret.Data["ttl"].(json.Number).Int64()
if err != nil {
t.Fatal(err)
}
if ttl > 6 {
t.Fatalf("unexpected ttl: %v", secret.Data["ttl"])
}
}
// Write new values
client.SetToken(origToken)
logger.Trace("origToken set into client", "origToken", origToken)
if bindSecretID {
resp, err = client.Logical().Write("auth/approle/role/test1/secret-id", nil)
if err != nil {
t.Fatal(err)
}
secretID2 := resp.WrapInfo.Token
if err := ioutil.WriteFile(secret, []byte(secretID2), 0o600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote test secret 2", "path", secret)
}
} else {
logger.Trace("skipped writing test secret 2")
}
newEntity := checkToken()
if newEntity != origEntity {
t.Fatal("did not find same entity")
}
timeout = time.Now().Add(4 * time.Second)
for {
if time.Now().After(timeout) {
break
}
secret, err := client.Auth().Token().LookupSelf()
if err != nil {
t.Fatal(err)
}
ttl, err := secret.Data["ttl"].(json.Number).Int64()
if err != nil {
t.Fatal(err)
}
if ttl > 6 {
t.Fatalf("unexpected ttl: %v", secret.Data["ttl"])
}
}
}
func addConstraints(add bool, cfg map[string]interface{}) map[string]interface{} {
if add {
// extraConstraints to add when bind_secret_id=false (otherwise Vault would fail with: "at least one constraint should be enabled on the role")
extraConstraints := map[string]interface{}{
"secret_id_bound_cidrs": "127.0.0.1/32",
"token_bound_cidrs": "127.0.0.1/32",
}
for k, v := range extraConstraints {
cfg[k] = v
}
}
return cfg
}