open-vault/builtin/credential/aws/backend_test.go

1914 lines
56 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package awsauth
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/policyutil"
"github.com/hashicorp/vault/sdk/logical"
)
const (
testVaultHeaderValue = "VaultAcceptanceTesting"
testValidRoleName = "valid-role"
testInvalidRoleName = "invalid-role"
)
func TestBackend_CreateParseVerifyRoleTag(t *testing.T) {
// create a backend
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
// create a role entry
data := map[string]interface{}{
"auth_type": "ec2",
"policies": "p,q,r,s",
"bound_ami_id": "abcd-123",
}
resp, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.CreateOperation,
Path: "role/abcd-123",
Storage: storage,
Data: data,
})
if resp != nil && resp.IsError() {
t.Fatalf("failed to create role")
}
if err != nil {
t.Fatal(err)
}
// read the created role entry
roleEntry, err := b.role(context.Background(), storage, "abcd-123")
if err != nil {
t.Fatal(err)
}
// create a nonce for the role tag
nonce, err := createRoleTagNonce()
if err != nil {
t.Fatal(err)
}
rTag1 := &roleTag{
Version: "v1",
Role: "abcd-123",
Nonce: nonce,
Policies: []string{"p", "q", "r"},
MaxTTL: 200000000000, // 200s
}
// create a role tag against the role entry
val, err := createRoleTagValue(rTag1, roleEntry)
if err != nil {
t.Fatal(err)
}
if val == "" {
t.Fatalf("failed to create role tag")
}
// parse the created role tag
rTag2, err := b.parseAndVerifyRoleTagValue(context.Background(), storage, val)
if err != nil {
t.Fatal(err)
}
// check the values in parsed role tag
if rTag2.Version != "v1" ||
rTag2.Nonce != nonce ||
rTag2.Role != "abcd-123" ||
rTag2.MaxTTL != 200000000000 || // 200s
!policyutil.EquivalentPolicies(rTag2.Policies, []string{"p", "q", "r"}) ||
len(rTag2.HMAC) == 0 {
t.Fatalf("parsed role tag is invalid")
}
// verify the tag contents using role specific HMAC key
verified, err := verifyRoleTagValue(rTag2, roleEntry)
if err != nil {
t.Fatal(err)
}
if !verified {
t.Fatalf("failed to verify the role tag")
}
// register a different role
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.CreateOperation,
Path: "role/ami-6789",
Storage: storage,
Data: data,
})
if resp != nil && resp.IsError() {
t.Fatalf("failed to create role")
}
if err != nil {
t.Fatal(err)
}
// get the entry of the newly created role entry
roleEntry2, err := b.role(context.Background(), storage, "ami-6789")
if err != nil {
t.Fatal(err)
}
// try to verify the tag created with previous role's HMAC key
// with the newly registered entry's HMAC key
verified, err = verifyRoleTagValue(rTag2, roleEntry2)
if err != nil {
t.Fatal(err)
}
if verified {
t.Fatalf("verification of role tag should have failed")
}
// modify any value in role tag and try to verify it
rTag2.Version = "v2"
verified, err = verifyRoleTagValue(rTag2, roleEntry)
if err != nil {
t.Fatal(err)
}
if verified {
t.Fatalf("verification of role tag should have failed: invalid Version")
}
}
func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) {
// create a nonce for the role tag
nonce, err := createRoleTagNonce()
if err != nil {
t.Fatal(err)
}
rTag := &roleTag{
Version: "v1",
Nonce: nonce,
Role: "abcd-123",
}
rTag.Version = ""
// try to create plaintext part of role tag
// without specifying version
val, err := prepareRoleTagPlaintextValue(rTag)
if err == nil {
t.Fatalf("expected error for missing version")
}
rTag.Version = "v1"
rTag.Nonce = ""
// try to create plaintext part of role tag
// without specifying nonce
val, err = prepareRoleTagPlaintextValue(rTag)
if err == nil {
t.Fatalf("expected error for missing nonce")
}
rTag.Nonce = nonce
rTag.Role = ""
// try to create plaintext part of role tag
// without specifying role
val, err = prepareRoleTagPlaintextValue(rTag)
if err == nil {
t.Fatalf("expected error for missing role")
}
rTag.Role = "abcd-123"
// create the plaintext part of the tag
val, err = prepareRoleTagPlaintextValue(rTag)
if err != nil {
t.Fatal(err)
}
// verify if it contains known fields
if !strings.Contains(val, "r=") ||
!strings.Contains(val, "d=") ||
!strings.Contains(val, "m=") ||
!strings.HasPrefix(val, "v1") {
t.Fatalf("incorrect information in role tag plaintext value")
}
rTag.InstanceID = "instance-123"
// create the role tag with instance_id specified
val, err = prepareRoleTagPlaintextValue(rTag)
if err != nil {
t.Fatal(err)
}
// verify it
if !strings.Contains(val, "i=") {
t.Fatalf("missing instance ID in role tag plaintext value")
}
rTag.MaxTTL = 200000000000
// create the role tag with max_ttl specified
val, err = prepareRoleTagPlaintextValue(rTag)
if err != nil {
t.Fatal(err)
}
// verify it
if !strings.Contains(val, "t=") {
t.Fatalf("missing max_ttl field in role tag plaintext value")
}
}
func TestBackend_CreateRoleTagNonce(t *testing.T) {
// create a nonce for the role tag
nonce, err := createRoleTagNonce()
if err != nil {
t.Fatal(err)
}
if nonce == "" {
t.Fatalf("failed to create role tag nonce")
}
// verify that the value returned is base64 encoded
nonceBytes, err := base64.StdEncoding.DecodeString(nonce)
if err != nil {
t.Fatal(err)
}
if len(nonceBytes) == 0 {
t.Fatalf("length of role tag nonce is zero")
}
}
func TestBackend_ConfigTidyIdentities(t *testing.T) {
for _, path := range []string{"config/tidy/identity-whitelist", "config/tidy/identity-accesslist"} {
// create a backend
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
// test update operation
tidyRequest := &logical.Request{
Operation: logical.UpdateOperation,
Path: path,
Storage: storage,
}
data := map[string]interface{}{
"safety_buffer": "60",
"disable_periodic_tidy": true,
}
tidyRequest.Data = data
_, err = b.HandleRequest(context.Background(), tidyRequest)
if err != nil {
t.Fatal(err)
}
// test read operation
tidyRequest.Operation = logical.ReadOperation
resp, err := b.HandleRequest(context.Background(), tidyRequest)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.IsError() {
t.Fatalf("failed to read %q endpoint", path)
}
if resp.Data["safety_buffer"].(int) != 60 || !resp.Data["disable_periodic_tidy"].(bool) {
t.Fatalf("bad: expected: safety_buffer:60 disable_periodic_tidy:true actual: safety_buffer:%d disable_periodic_tidy:%t\n", resp.Data["safety_buffer"].(int), resp.Data["disable_periodic_tidy"].(bool))
}
// test delete operation
tidyRequest.Operation = logical.DeleteOperation
resp, err = b.HandleRequest(context.Background(), tidyRequest)
if err != nil {
t.Fatal(err)
}
if resp != nil {
t.Fatalf("failed to delete %q", path)
}
}
}
func TestBackend_ConfigTidyRoleTags(t *testing.T) {
for _, path := range []string{"config/tidy/roletag-blacklist", "config/tidy/roletag-denylist"} {
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
// test update operation
tidyRequest := &logical.Request{
Operation: logical.UpdateOperation,
Path: path,
Storage: storage,
}
data := map[string]interface{}{
"safety_buffer": "60",
"disable_periodic_tidy": true,
}
tidyRequest.Data = data
_, err = b.HandleRequest(context.Background(), tidyRequest)
if err != nil {
t.Fatal(err)
}
// test read operation
tidyRequest.Operation = logical.ReadOperation
resp, err := b.HandleRequest(context.Background(), tidyRequest)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.IsError() {
t.Fatalf("failed to read %s endpoint", path)
}
if resp.Data["safety_buffer"].(int) != 60 || !resp.Data["disable_periodic_tidy"].(bool) {
t.Fatalf("bad: expected: safety_buffer:60 disable_periodic_tidy:true actual: safety_buffer:%d disable_periodic_tidy:%t\n", resp.Data["safety_buffer"].(int), resp.Data["disable_periodic_tidy"].(bool))
}
// test delete operation
tidyRequest.Operation = logical.DeleteOperation
resp, err = b.HandleRequest(context.Background(), tidyRequest)
if err != nil {
t.Fatal(err)
}
if resp != nil {
t.Fatalf("failed to delete %s", path)
}
}
}
func TestBackend_TidyIdentities(t *testing.T) {
for _, path := range []string{"tidy/identity-whitelist", "tidy/identity-accesslist"} {
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
expiredIdentityWhitelist := &accessListIdentity{
ExpirationTime: time.Now().Add(-1 * 24 * 365 * time.Hour),
}
entry, err := logical.StorageEntryJSON("whitelist/identity/id1", expiredIdentityWhitelist)
if err != nil {
t.Fatal(err)
}
if err := storage.Put(context.Background(), entry); err != nil {
t.Fatal(err)
}
// test update operation
_, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: path,
Storage: storage,
})
if err != nil {
t.Fatal(err)
}
// let tidy finish in the background
time.Sleep(1 * time.Second)
entry, err = storage.Get(context.Background(), "whitelist/identity/id1")
if err != nil {
t.Fatal(err)
}
if entry != nil {
t.Fatal("wl tidy did not remove expired entry")
}
}
}
func TestBackend_TidyRoleTags(t *testing.T) {
for _, path := range []string{"tidy/roletag-blacklist", "tidy/roletag-denylist"} {
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
expiredIdentityWhitelist := &roleTagBlacklistEntry{
ExpirationTime: time.Now().Add(-1 * 24 * 365 * time.Hour),
}
entry, err := logical.StorageEntryJSON("blacklist/roletag/id1", expiredIdentityWhitelist)
if err != nil {
t.Fatal(err)
}
if err := storage.Put(context.Background(), entry); err != nil {
t.Fatal(err)
}
// test update operation
_, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: path,
Storage: storage,
})
if err != nil {
t.Fatal(err)
}
// let tidy finish in the background
time.Sleep(1 * time.Second)
entry, err = storage.Get(context.Background(), "blacklist/roletag/id1")
if err != nil {
t.Fatal(err)
}
if entry != nil {
t.Fatal("bl tidy did not remove expired entry")
}
}
}
func TestBackend_ConfigClient(t *testing.T) {
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
data := map[string]interface{}{
"access_key": "AKIAJBRHKV6EVTTNXDHA",
"secret_key": "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",
}
stepCreate := logicaltest.TestStep{
Operation: logical.CreateOperation,
Path: "config/client",
Data: data,
}
stepUpdate := logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config/client",
Data: data,
}
data3 := map[string]interface{}{
"access_key": "",
"secret_key": "mCtSM8ZUEQ3mOFVZYPBQkf2sO6F/W7a5TVzrl3Oj",
}
stepInvalidAccessKey := logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config/client",
Data: data3,
ErrorOk: true,
}
data4 := map[string]interface{}{
"access_key": "accesskey",
"secret_key": "",
}
stepInvalidSecretKey := logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config/client",
Data: data4,
ErrorOk: true,
}
logicaltest.Test(t, logicaltest.TestCase{
AcceptanceTest: false,
CredentialBackend: b,
Steps: []logicaltest.TestStep{
stepCreate,
stepInvalidAccessKey,
stepInvalidSecretKey,
stepUpdate,
},
})
// test existence check returning false
checkFound, exists, err := b.HandleExistenceCheck(context.Background(), &logical.Request{
Operation: logical.CreateOperation,
Path: "config/client",
Storage: storage,
})
if err != nil {
t.Fatal(err)
}
if !checkFound {
t.Fatal("existence check not found for path 'config/client'")
}
if exists {
t.Fatal("existence check should have returned 'false' for 'config/client'")
}
// create an entry
configClientCreateRequest := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/client",
Data: data,
Storage: storage,
}
_, err = b.HandleRequest(context.Background(), configClientCreateRequest)
if err != nil {
t.Fatal(err)
}
// test existence check returning true
checkFound, exists, err = b.HandleExistenceCheck(context.Background(), &logical.Request{
Operation: logical.CreateOperation,
Path: "config/client",
Storage: storage,
})
if err != nil {
t.Fatal(err)
}
if !checkFound {
t.Fatal("existence check not found for path 'config/client'")
}
if !exists {
t.Fatal("existence check should have returned 'true' for 'config/client'")
}
endpointData := map[string]interface{}{
"secret_key": "secretkey",
"access_key": "accesskey",
"endpoint": "endpointvalue",
}
endpointReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/client",
Storage: storage,
Data: endpointData,
}
_, err = b.HandleRequest(context.Background(), endpointReq)
if err != nil {
t.Fatal(err)
}
endpointReq.Operation = logical.ReadOperation
resp, err := b.HandleRequest(context.Background(), endpointReq)
if err != nil {
t.Fatal(err)
}
if resp == nil ||
resp.IsError() {
t.Fatalf("")
}
actual := resp.Data["endpoint"].(string)
if actual != "endpointvalue" {
t.Fatalf("bad: endpoint: expected:endpointvalue actual:%s\n", actual)
}
}
func TestBackend_pathConfigCertificate(t *testing.T) {
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
certReq := &logical.Request{
Operation: logical.CreateOperation,
Storage: storage,
Path: "config/certificate/cert1",
}
checkFound, exists, err := b.HandleExistenceCheck(context.Background(), certReq)
if err != nil {
t.Fatal(err)
}
if !checkFound {
t.Fatal("existence check not found for path 'config/certificate/cert1'")
}
if exists {
t.Fatal("existence check should have returned 'false' for 'config/certificate/cert1'")
}
data := map[string]interface{}{
"type": "pkcs7",
"aws_public_cert": `LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM3VENDQXEwQ0NRQ1d1a2paNVY0YVp6QUpC
Z2NxaGtqT09BUURNRnd4Q3pBSkJnTlZCQVlUQWxWVE1Sa3cKRndZRFZRUUlFeEJYWVhOb2FXNW5k
Rzl1SUZOMFlYUmxNUkF3RGdZRFZRUUhFd2RUWldGMGRHeGxNU0F3SGdZRApWUVFLRXhkQmJXRjZi
MjRnVjJWaUlGTmxjblpwWTJWeklFeE1RekFlRncweE1qQXhNRFV4TWpVMk1USmFGdzB6Ck9EQXhN
RFV4TWpVMk1USmFNRnd4Q3pBSkJnTlZCQVlUQWxWVE1Sa3dGd1lEVlFRSUV4QlhZWE5vYVc1bmRH
OXUKSUZOMFlYUmxNUkF3RGdZRFZRUUhFd2RUWldGMGRHeGxNU0F3SGdZRFZRUUtFeGRCYldGNmIy
NGdWMlZpSUZObApjblpwWTJWeklFeE1RekNDQWJjd2dnRXNCZ2NxaGtqT09BUUJNSUlCSHdLQmdR
Q2prdmNTMmJiMVZRNHl0LzVlCmloNU9PNmtLL24xTHpsbHI3RDhad3RRUDhmT0VwcDVFMm5nK0Q2
VWQxWjFnWWlwcjU4S2ozbnNzU05wSTZiWDMKVnlJUXpLN3dMY2xuZC9Zb3pxTk5tZ0l5WmVjTjdF
Z2xLOUlUSEpMUCt4OEZ0VXB0M1FieVlYSmRtVk1lZ042UApodmlZdDVKSC9uWWw0aGgzUGExSEpk
c2tnUUlWQUxWSjNFUjExK0tvNHRQNm53dkh3aDYrRVJZUkFvR0JBSTFqCmsrdGtxTVZIdUFGY3ZB
R0tvY1Rnc2pKZW02LzVxb216SnVLRG1iSk51OVF4dzNyQW90WGF1OFFlK01CY0psL1UKaGh5MUtI
VnBDR2w5ZnVlUTJzNklMMENhTy9idXljVTFDaVlRazQwS05IQ2NIZk5pWmJkbHgxRTlycFVwN2Ju
RgpsUmEydjFudE1YM2NhUlZEZGJ0UEVXbWR4U0NZc1lGRGs0bVpyT0xCQTRHRUFBS0JnRWJtZXZl
NWY4TElFL0dmCk1ObVA5Q001ZW92UU9HeDVobzhXcUQrYVRlYnMrazJ0bjkyQkJQcWVacXBXUmE1
UC8ranJkS21sMXF4NGxsSFcKTVhyczNJZ0liNitoVUlCK1M4ZHo4L21tTzBicHI3NlJvWlZDWFlh
YjJDWmVkRnV0N3FjM1dVSDkrRVVBSDVtdwp2U2VEQ09VTVlRUjdSOUxJTll3b3VISXppcVFZTUFr
R0J5cUdTTTQ0QkFNREx3QXdMQUlVV1hCbGs0MHhUd1N3CjdIWDMyTXhYWXJ1c2U5QUNGQk5HbWRY
MlpCclZOR3JOOU4yZjZST2swazlLCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
`,
}
certReq.Data = data
// test create operation
resp, err := b.HandleRequest(context.Background(), certReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v, err: %v", resp, err)
}
certReq.Data = nil
// test existence check
checkFound, exists, err = b.HandleExistenceCheck(context.Background(), certReq)
if err != nil {
t.Fatal(err)
}
if !checkFound {
t.Fatal("existence check not found for path 'config/certificate/cert1'")
}
if !exists {
t.Fatal("existence check should have returned 'true' for 'config/certificate/cert1'")
}
certReq.Operation = logical.ReadOperation
// test read operation
resp, err = b.HandleRequest(context.Background(), certReq)
if err != nil {
t.Fatal(err)
}
expectedCert := `-----BEGIN CERTIFICATE-----
MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw
FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD
VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z
ODAxMDUxMjU2MTJaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9u
IFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNl
cnZpY2VzIExMQzCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQCjkvcS2bb1VQ4yt/5e
ih5OO6kK/n1Lzllr7D8ZwtQP8fOEpp5E2ng+D6Ud1Z1gYipr58Kj3nssSNpI6bX3
VyIQzK7wLclnd/YozqNNmgIyZecN7EglK9ITHJLP+x8FtUpt3QbyYXJdmVMegN6P
hviYt5JH/nYl4hh3Pa1HJdskgQIVALVJ3ER11+Ko4tP6nwvHwh6+ERYRAoGBAI1j
k+tkqMVHuAFcvAGKocTgsjJem6/5qomzJuKDmbJNu9Qxw3rAotXau8Qe+MBcJl/U
hhy1KHVpCGl9fueQ2s6IL0CaO/buycU1CiYQk40KNHCcHfNiZbdlx1E9rpUp7bnF
lRa2v1ntMX3caRVDdbtPEWmdxSCYsYFDk4mZrOLBA4GEAAKBgEbmeve5f8LIE/Gf
MNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW
MXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw
vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw
7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K
-----END CERTIFICATE-----
`
if resp.Data["aws_public_cert"].(string) != expectedCert {
t.Fatalf("bad: expected:%s\n got:%s\n", expectedCert, resp.Data["aws_public_cert"].(string))
}
certReq.Operation = logical.CreateOperation
certReq.Path = "config/certificate/cert2"
certReq.Data = data
// create another entry to test the list operation
_, err = b.HandleRequest(context.Background(), certReq)
if err != nil {
t.Fatal(err)
}
certReq.Operation = logical.ListOperation
certReq.Path = "config/certificates"
// test list operation
resp, err = b.HandleRequest(context.Background(), certReq)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.IsError() {
t.Fatalf("failed to list config/certificates")
}
keys := resp.Data["keys"].([]string)
if len(keys) != 2 {
t.Fatalf("invalid keys listed: %#v\n", keys)
}
certReq.Operation = logical.DeleteOperation
certReq.Path = "config/certificate/cert1"
_, err = b.HandleRequest(context.Background(), certReq)
if err != nil {
t.Fatal(err)
}
certReq.Path = "config/certificate/cert2"
_, err = b.HandleRequest(context.Background(), certReq)
if err != nil {
t.Fatal(err)
}
certReq.Operation = logical.ListOperation
certReq.Path = "config/certificates"
// test list operation
resp, err = b.HandleRequest(context.Background(), certReq)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.IsError() {
t.Fatalf("failed to list config/certificates")
}
if resp.Data["keys"] != nil {
t.Fatalf("no entries should be present")
}
}
func TestBackend_parseAndVerifyRoleTagValue(t *testing.T) {
// create a backend
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
// create a role
data := map[string]interface{}{
"auth_type": "ec2",
"policies": "p,q,r,s",
"max_ttl": "120s",
"role_tag": "VaultRole",
"bound_ami_id": "abcd-123",
}
resp, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.CreateOperation,
Path: "role/abcd-123",
Storage: storage,
Data: data,
})
if resp != nil && resp.IsError() {
t.Fatalf("failed to create role")
}
if err != nil {
t.Fatal(err)
}
// verify that the entry is created
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Path: "role/abcd-123",
Storage: storage,
})
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatalf("expected an role entry for abcd-123")
}
// create a role tag
data2 := map[string]interface{}{
"policies": "p,q,r,s",
}
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "role/abcd-123/tag",
Storage: storage,
Data: data2,
})
if err != nil {
t.Fatal(err)
}
if resp.Data["tag_key"].(string) == "" ||
resp.Data["tag_value"].(string) == "" {
t.Fatalf("invalid tag response: %#v\n", resp)
}
tagValue := resp.Data["tag_value"].(string)
// parse the value and check if the verifiable values match
rTag, err := b.parseAndVerifyRoleTagValue(context.Background(), storage, tagValue)
if err != nil {
t.Fatalf("err: %s", err)
}
if rTag == nil {
t.Fatalf("failed to parse role tag")
}
if rTag.Version != "v1" ||
!policyutil.EquivalentPolicies(rTag.Policies, []string{"p", "q", "r", "s"}) ||
rTag.Role != "abcd-123" {
t.Fatalf("bad: parsed role tag contains incorrect values. Got: %#v\n", rTag)
}
}
func TestBackend_PathRoleTag(t *testing.T) {
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
data := map[string]interface{}{
"auth_type": "ec2",
"policies": "p,q,r,s",
"max_ttl": "120s",
"role_tag": "VaultRole",
"bound_ami_id": "abcd-123",
}
resp, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.CreateOperation,
Path: "role/abcd-123",
Storage: storage,
Data: data,
})
if resp != nil && resp.IsError() {
t.Fatalf("failed to create role")
}
if err != nil {
t.Fatal(err)
}
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Path: "role/abcd-123",
Storage: storage,
})
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatalf("failed to find a role entry for abcd-123")
}
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "role/abcd-123/tag",
Storage: storage,
})
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Data == nil {
t.Fatalf("failed to create a tag on role: abcd-123")
}
if resp.IsError() {
t.Fatalf("failed to create a tag on role: abcd-123: %s\n", resp.Data["error"])
}
if resp.Data["tag_value"].(string) == "" {
t.Fatalf("role tag not present in the response data: %#v\n", resp.Data)
}
}
func TestBackend_PathBlacklistRoleTag(t *testing.T) {
for _, path := range []string{"roletag-blacklist/", "roletag-denylist/"} {
// create the backend
storage := &logical.InmemStorage{}
config := logical.TestBackendConfig()
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
// create an role entry
data := map[string]interface{}{
"auth_type": "ec2",
"policies": "p,q,r,s",
"role_tag": "VaultRole",
"bound_ami_id": "abcd-123",
}
resp, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.CreateOperation,
Path: "role/abcd-123",
Storage: storage,
Data: data,
})
if resp != nil && resp.IsError() {
t.Fatalf("failed to create role")
}
if err != nil {
t.Fatal(err)
}
// create a role tag against an role registered before
data2 := map[string]interface{}{
"policies": "p,q,r,s",
}
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "role/abcd-123/tag",
Storage: storage,
Data: data2,
})
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Data == nil {
t.Fatalf("failed to create a tag on role: abcd-123")
}
if resp.IsError() {
t.Fatalf("failed to create a tag on role: abcd-123: %s\n", resp.Data["error"])
}
tag := resp.Data["tag_value"].(string)
if tag == "" {
t.Fatalf("role tag not present in the response data: %#v\n", resp.Data)
}
// deny list that role tag
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: path + tag,
Storage: storage,
})
if err != nil {
t.Fatal(err)
}
if resp != nil {
t.Fatalf("failed to deny list the roletag: %s\n", tag)
}
// read the deny list entry
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Path: path + tag,
Storage: storage,
})
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Data == nil {
t.Fatalf("failed to read the deny list role tag: %s\n", tag)
}
if resp.IsError() {
t.Fatalf("failed to read the deny list role tag:%s. Err: %s\n", tag, resp.Data["error"])
}
// delete the deny listed entry
_, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.DeleteOperation,
Path: path + tag,
Storage: storage,
})
if err != nil {
t.Fatal(err)
}
// try to read the deleted entry
tagEntry, err := b.lockedDenyLististRoleTagEntry(context.Background(), storage, tag)
if err != nil {
t.Fatal(err)
}
if tagEntry != nil {
t.Fatalf("role tag should not have been present: %s\n", tag)
}
}
}
/*
This is an acceptance test.
Requires the following env vars:
TEST_AWS_EC2_RSA2048
TEST_AWS_EC2_PKCS7
TEST_AWS_EC2_IDENTITY_DOCUMENT
TEST_AWS_EC2_IDENTITY_DOCUMENT_SIG
TEST_AWS_EC2_AMI_ID
TEST_AWS_EC2_ACCOUNT_ID
TEST_AWS_EC2_IAM_ROLE_ARN
If this is being run on an EC2 instance, you can set the environment vars using this bash snippet:
export TEST_AWS_EC2_RSA2048=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/rsa2048)
export TEST_AWS_EC2_PKCS7=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/pkcs7)
export TEST_AWS_EC2_IDENTITY_DOCUMENT=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | base64 -w 0)
export TEST_AWS_EC2_IDENTITY_DOCUMENT_SIG=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/signature | tr -d '\n')
export TEST_AWS_EC2_AMI_ID=$(curl -s http://169.254.169.254/latest/meta-data/ami-id)
export TEST_AWS_EC2_IAM_ROLE_ARN=$(aws iam get-role --role-name $(curl -q http://169.254.169.254/latest/meta-data/iam/security-credentials/ -S -s) --query Role.Arn --output text)
export TEST_AWS_EC2_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
If the test is not being run on an EC2 instance that has access to
credentials using EC2RoleProvider, on top of the above vars, following
needs to be set:
TEST_AWS_SECRET_KEY
TEST_AWS_ACCESS_KEY
*/
func TestBackendAcc_LoginWithInstanceIdentityDocAndAccessListIdentity(t *testing.T) {
for _, path := range []string{"identity-whitelist/", "identity-accesslist/"} {
// This test case should be run only when certain env vars are set and
// executed as an acceptance test.
if os.Getenv(logicaltest.TestEnvVar) == "" {
t.Skip(fmt.Sprintf("Acceptance tests skipped unless env %q set", logicaltest.TestEnvVar))
return
}
rsa2048 := os.Getenv("TEST_AWS_EC2_RSA2048")
if rsa2048 == "" {
t.Skipf("env var TEST_AWS_EC2_RSA2048 not set, skipping test")
}
pkcs7 := os.Getenv("TEST_AWS_EC2_PKCS7")
if pkcs7 == "" {
t.Skipf("env var TEST_AWS_EC2_PKCS7 not set, skipping test")
}
identityDoc := os.Getenv("TEST_AWS_EC2_IDENTITY_DOCUMENT")
if identityDoc == "" {
t.Skipf("env var TEST_AWS_EC2_IDENTITY_DOCUMENT not set, skipping test")
}
identityDocSig := os.Getenv("TEST_AWS_EC2_IDENTITY_DOCUMENT_SIG")
if identityDocSig == "" {
t.Skipf("env var TEST_AWS_EC2_IDENTITY_DOCUMENT_SIG not set, skipping test")
}
amiID := os.Getenv("TEST_AWS_EC2_AMI_ID")
if amiID == "" {
t.Skipf("env var TEST_AWS_EC2_AMI_ID not set, skipping test")
}
iamARN := os.Getenv("TEST_AWS_EC2_IAM_ROLE_ARN")
if iamARN == "" {
t.Skipf("env var TEST_AWS_EC2_IAM_ROLE_ARN not set, skipping test")
}
accountID := os.Getenv("TEST_AWS_EC2_ACCOUNT_ID")
if accountID == "" {
t.Skipf("env var TEST_AWS_EC2_ACCOUNT_ID not set, skipping test")
}
roleName := amiID
// create the backend
storage := &logical.InmemStorage{}
config := logical.TestBackendConfig()
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
accessKey := os.Getenv("TEST_AWS_ACCESS_KEY")
secretKey := os.Getenv("TEST_AWS_SECRET_KEY")
// In case of problems with making API calls using the credentials (2FA enabled,
// for instance), the keys need not be set if the test is running on an EC2
// instance with permissions to get the credentials using EC2RoleProvider.
if accessKey != "" && secretKey != "" {
// get the API credentials from env vars
clientConfig := map[string]interface{}{
"access_key": accessKey,
"secret_key": secretKey,
}
if clientConfig["access_key"] == "" ||
clientConfig["secret_key"] == "" {
t.Fatalf("credentials not configured")
}
// store the credentials
_, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Storage: storage,
Path: "config/client",
Data: clientConfig,
})
if err != nil {
t.Fatal(err)
}
}
// Configure additional metadata to be returned for ec2 logins.
identity := map[string]interface{}{
"ec2_metadata": []string{"instance_id", "region", "ami_id"},
}
// store the identity
_, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Storage: storage,
Path: "config/identity",
Data: identity,
})
if err != nil {
t.Fatal(err)
}
loginInput := map[string]interface{}{
"pkcs7": pkcs7,
"nonce": "vault-client-nonce",
}
parsedIdentityDoc, err := b.parseIdentityDocument(context.Background(), storage, pkcs7)
if err != nil {
t.Fatal(err)
}
// Perform the login operation with a AMI ID that is not matching
// the bound on the role.
loginRequest := &logical.Request{
Operation: logical.UpdateOperation,
Path: "login",
Storage: storage,
Data: loginInput,
}
// Baseline role data that should succeed permit login
data := map[string]interface{}{
"auth_type": "ec2",
"policies": "root",
"max_ttl": "120s",
"bound_ami_id": []string{"wrong_ami_id", amiID, "wrong_ami_id2"},
"bound_account_id": accountID,
"bound_iam_role_arn": iamARN,
"bound_ec2_instance_id": []string{parsedIdentityDoc.InstanceID, "i-1234567"},
}
roleReq := &logical.Request{
Operation: logical.CreateOperation,
Path: "role/" + roleName,
Storage: storage,
Data: data,
}
updateRoleExpectLoginFail := func(roleRequest, loginRequest *logical.Request) error {
resp, err := b.HandleRequest(context.Background(), roleRequest)
if err != nil || (resp != nil && resp.IsError()) {
return fmt.Errorf("bad: failed to create role: resp:%#v\nerr:%v", resp, err)
}
resp, err = b.HandleRequest(context.Background(), loginRequest)
if err != nil || resp == nil || (resp != nil && !resp.IsError()) {
return fmt.Errorf("bad: expected login failure: resp:%#v\nerr:%v", resp, err)
}
return nil
}
// Test a role with the wrong AMI ID
data["bound_ami_id"] = []string{"ami-1234567", "ami-7654321"}
if err := updateRoleExpectLoginFail(roleReq, loginRequest); err != nil {
t.Fatal(err)
}
roleReq.Operation = logical.UpdateOperation
// Place the correct AMI ID in one of the values, but make the AccountID wrong
data["bound_ami_id"] = []string{"wrong_ami_id_1", amiID, "wrong_ami_id_2"}
data["bound_account_id"] = []string{"wrong-account-id", "wrong-account-id-2"}
if err := updateRoleExpectLoginFail(roleReq, loginRequest); err != nil {
t.Fatal(err)
}
// Place the correct AccountID in one of the values, but make the wrong IAMRoleARN
data["bound_account_id"] = []string{"wrong-account-id-1", accountID, "wrong-account-id-2"}
data["bound_iam_role_arn"] = []string{"wrong_iam_role_arn", "wrong_iam_role_arn_2"}
if err := updateRoleExpectLoginFail(roleReq, loginRequest); err != nil {
t.Fatal(err)
}
// Place correct IAM role ARN, but incorrect instance ID
data["bound_iam_role_arn"] = []string{"wrong_iam_role_arn_1", iamARN, "wrong_iam_role_arn_2"}
data["bound_ec2_instance_id"] = "i-1234567"
if err := updateRoleExpectLoginFail(roleReq, loginRequest); err != nil {
t.Fatal(err)
}
// Place correct instance ID, but substring of the IAM role ARN
data["bound_ec2_instance_id"] = []string{parsedIdentityDoc.InstanceID, "i-1234567"}
data["bound_iam_role_arn"] = []string{"wrong_iam_role_arn", iamARN[:len(iamARN)-2], "wrong_iam_role_arn_2"}
if err := updateRoleExpectLoginFail(roleReq, loginRequest); err != nil {
t.Fatal(err)
}
// place a wildcard in the middle of the role ARN
// The :31 gets arn:aws:iam::123456789012:role/
// This test relies on the role name having at least two characters
data["bound_iam_role_arn"] = []string{"wrong_iam_role_arn", fmt.Sprintf("%s*%s", iamARN[:31], iamARN[32:])}
if err := updateRoleExpectLoginFail(roleReq, loginRequest); err != nil {
t.Fatal(err)
}
// globbed IAM role ARN
data["bound_iam_role_arn"] = []string{"wrong_iam_role_arn_1", fmt.Sprintf("%s*", iamARN[:len(iamARN)-2]), "wrong_iam_role_arn_2"}
resp, err := b.HandleRequest(context.Background(), roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err)
}
// Now, the login attempt should succeed
resp, err = b.HandleRequest(context.Background(), loginRequest)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Auth == nil || resp.IsError() {
t.Fatalf("bad: failed to login: resp:%#v\nerr:%v", resp, err)
}
// Attempt to re-login with the identity signature
delete(loginInput, "pkcs7")
loginInput["identity"] = identityDoc
loginInput["signature"] = identityDocSig
resp, err = b.HandleRequest(context.Background(), loginRequest)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Auth == nil || resp.IsError() {
t.Fatalf("bad: failed to login: resp:%#v\nerr:%v", resp, err)
}
// verify the presence of instance_id in the response object.
instanceID := resp.Auth.Metadata["instance_id"]
if instanceID == "" {
t.Fatalf("instance ID not present in the response object")
}
if instanceID != parsedIdentityDoc.InstanceID {
t.Fatalf("instance ID in response (%q) did not match instance ID from identity document (%q)", instanceID, parsedIdentityDoc.InstanceID)
}
_, ok := resp.Auth.Metadata["nonce"]
if ok {
t.Fatalf("client nonce should not have been returned")
}
loginInput["nonce"] = "changed-vault-client-nonce"
// try to login again with changed nonce
resp, err = b.HandleRequest(context.Background(), loginRequest)
if err != nil {
t.Fatal(err)
}
if resp == nil || !resp.IsError() {
t.Fatalf("login attempt should have failed due to client nonce mismatch")
}
// Check if an access list identity entry is created after the login.
wlRequest := &logical.Request{
Operation: logical.ReadOperation,
Path: path + instanceID,
Storage: storage,
}
resp, err = b.HandleRequest(context.Background(), wlRequest)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Data == nil || resp.Data["role"] != roleName {
t.Fatalf("failed to read access list identity")
}
// Delete the access list identity entry.
wlRequest.Operation = logical.DeleteOperation
resp, err = b.HandleRequest(context.Background(), wlRequest)
if err != nil {
t.Fatal(err)
}
if resp.IsError() {
t.Fatalf("failed to delete access list identity")
}
// Allow a fresh login without supplying the nonce
delete(loginInput, "nonce")
resp, err = b.HandleRequest(context.Background(), loginRequest)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Auth == nil || resp.IsError() {
t.Fatalf("login attempt failed")
}
_, ok = resp.Auth.Metadata["nonce"]
if !ok {
t.Fatalf("expected nonce to be returned")
}
// Attempt to re-login with the rsa2048 signature as a pkcs7 signature
wlRequest.Operation = logical.DeleteOperation
resp, err = b.HandleRequest(context.Background(), wlRequest)
if err != nil {
t.Fatal(err)
}
if resp.IsError() {
t.Fatalf("failed to delete access list identity")
}
delete(loginInput, "identity")
delete(loginInput, "signature")
loginInput["pkcs7"] = rsa2048
resp, err = b.HandleRequest(context.Background(), loginRequest)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Auth == nil || resp.IsError() {
t.Fatalf("bad: failed to login: resp:%#v\nerr:%v", resp, err)
}
// verify the presence of instance_id in the response object.
instanceID = resp.Auth.Metadata["instance_id"]
if instanceID == "" {
t.Fatalf("instance ID not present in the response object")
}
if instanceID != parsedIdentityDoc.InstanceID {
t.Fatalf("instance ID in response (%q) did not match instance ID from identity document (%q)", instanceID, parsedIdentityDoc.InstanceID)
}
}
}
func TestBackend_pathStsConfig(t *testing.T) {
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
stsReq := &logical.Request{
Operation: logical.CreateOperation,
Storage: storage,
Path: "config/sts/account1",
}
checkFound, exists, err := b.HandleExistenceCheck(context.Background(), stsReq)
if err != nil {
t.Fatal(err)
}
if !checkFound {
t.Fatal("existence check not found for path 'config/sts/account1'")
}
if exists {
t.Fatal("existence check should have returned 'false' for 'config/sts/account1'")
}
data := map[string]interface{}{
"sts_role": "arn:aws:iam:account1:role/myRole",
}
stsReq.Data = data
// test create operation
resp, err := b.HandleRequest(context.Background(), stsReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("resp: %#v, err: %v", resp, err)
}
stsReq.Data = nil
// test existence check
checkFound, exists, err = b.HandleExistenceCheck(context.Background(), stsReq)
if err != nil {
t.Fatal(err)
}
if !checkFound {
t.Fatal("existence check not found for path 'config/sts/account1'")
}
if !exists {
t.Fatal("existence check should have returned 'true' for 'config/sts/account1'")
}
stsReq.Operation = logical.ReadOperation
// test read operation
resp, err = b.HandleRequest(context.Background(), stsReq)
if err != nil {
t.Fatal(err)
}
expectedStsRole := "arn:aws:iam:account1:role/myRole"
if resp.Data["sts_role"].(string) != expectedStsRole {
t.Fatalf("bad: expected:%s\n got:%s\n", expectedStsRole, resp.Data["sts_role"].(string))
}
stsReq.Operation = logical.CreateOperation
stsReq.Path = "config/sts/account2"
stsReq.Data = data
// create another entry to test the list operation
resp, err = b.HandleRequest(context.Background(), stsReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatal(err)
}
stsReq.Operation = logical.ListOperation
stsReq.Path = "config/sts"
// test list operation
resp, err = b.HandleRequest(context.Background(), stsReq)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.IsError() {
t.Fatalf("failed to list config/sts")
}
keys := resp.Data["keys"].([]string)
if len(keys) != 2 {
t.Fatalf("invalid keys listed: %#v\n", keys)
}
stsReq.Operation = logical.DeleteOperation
stsReq.Path = "config/sts/account1"
resp, err = b.HandleRequest(context.Background(), stsReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatal(err)
}
stsReq.Path = "config/sts/account2"
resp, err = b.HandleRequest(context.Background(), stsReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatal(err)
}
stsReq.Operation = logical.ListOperation
stsReq.Path = "config/sts"
// test list operation
resp, err = b.HandleRequest(context.Background(), stsReq)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.IsError() {
t.Fatalf("failed to list config/sts")
}
if resp.Data["keys"] != nil {
t.Fatalf("no entries should be present")
}
}
func buildCallerIdentityLoginData(request *http.Request, roleName string) (map[string]interface{}, error) {
headersJson, err := json.Marshal(request.Header)
if err != nil {
return nil, err
}
requestBody, err := ioutil.ReadAll(request.Body)
if err != nil {
return nil, err
}
return map[string]interface{}{
"iam_http_request_method": request.Method,
"iam_request_url": base64.StdEncoding.EncodeToString([]byte(request.URL.String())),
"iam_request_headers": base64.StdEncoding.EncodeToString(headersJson),
"iam_request_body": base64.StdEncoding.EncodeToString(requestBody),
"request_role": roleName,
}, nil
}
// This is an acceptance test.
// If the test is NOT being run on an AWS EC2 instance in an instance profile,
// it requires the following environment variables to be set:
// TEST_AWS_ACCESS_KEY_ID
// TEST_AWS_SECRET_ACCESS_KEY
// TEST_AWS_SECURITY_TOKEN or TEST_AWS_SESSION_TOKEN (optional, if you are using short-lived creds)
// These are intentionally NOT the "standard" variables to prevent accidentally
// using prod creds in acceptance tests
func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) {
// This test case should be run only when certain env vars are set and
// executed as an acceptance test.
if os.Getenv(logicaltest.TestEnvVar) == "" {
t.Skip(fmt.Sprintf("Acceptance tests skipped unless env %q set", logicaltest.TestEnvVar))
return
}
ctx := context.Background()
storage := &logical.InmemStorage{}
config := logical.TestBackendConfig()
config.StorageView = storage
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
err = b.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
// Override the default AWS env vars (if set) with our test creds
// so that the credential provider chain will pick them up
// NOTE that I'm not bothing to override the shared config file location,
// so if creds are specified there, they will be used before IAM
// instance profile creds
// This doesn't provide perfect leakage protection (e.g., it will still
// potentially pick up credentials from the ~/.config files), but probably
// good enough rather than having to muck around in the low-level details
for _, envvar := range []string{
"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SECURITY_TOKEN", "AWS_SESSION_TOKEN",
} {
// Skip test if any of the required env vars are missing
testEnvVar := os.Getenv("TEST_" + envvar)
if testEnvVar == "" {
t.Skipf("env var %s not set, skipping test", "TEST_"+envvar)
}
// restore existing environment variables (in case future tests need them)
defer os.Setenv(envvar, os.Getenv(envvar))
os.Setenv(envvar, testEnvVar)
}
awsSession, err := session.NewSession()
if err != nil {
fmt.Println("failed to create session,", err)
return
}
stsService := sts.New(awsSession)
stsInputParams := &sts.GetCallerIdentityInput{}
testIdentity, err := stsService.GetCallerIdentity(stsInputParams)
if err != nil {
t.Fatalf("Received error retrieving identity: %s", err)
}
entity, err := parseIamArn(*testIdentity.Arn)
if err != nil {
t.Fatal(err)
}
// Test setup largely done
// At this point, we're going to:
// 1. Configure the client to require our test header value
// 2. Configure identity to use the ARN for the alias
// 3. Configure two different roles:
// a. One bound to our test user
// b. One bound to a garbage ARN
// 4. Pass in a request that doesn't have the signed header, ensure
// we're not allowed to login
// 5. Passin a request that has a validly signed header, but the wrong
// value, ensure it doesn't allow login
// 6. Pass in a request that has a validly signed request, ensure
// it allows us to login to our role
// 7. Pass in a request that has a validly signed request, asking for
// the other role, ensure it fails
clientConfigData := map[string]interface{}{
"iam_server_id_header_value": testVaultHeaderValue,
}
clientRequest := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/client",
Storage: storage,
Data: clientConfigData,
}
_, err = b.HandleRequest(ctx, clientRequest)
if err != nil {
t.Fatal(err)
}
configIdentityData := map[string]interface{}{
"iam_alias": identityAliasIAMFullArn,
}
configIdentityRequest := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/identity",
Storage: storage,
Data: configIdentityData,
}
resp, err := b.HandleRequest(ctx, configIdentityRequest)
if err != nil {
t.Fatal(err)
}
if resp != nil && resp.IsError() {
t.Fatalf("received error response when configuring identity: %#v", resp)
}
// configuring the valid role we'll be able to login to
roleData := map[string]interface{}{
"bound_iam_principal_arn": []string{entity.canonicalArn(), "arn:aws:iam::123456789012:role/FakeRoleArn1*"}, // Fake ARN MUST be wildcard terminated because we're resolving unique IDs, and the wildcard termination prevents unique ID resolution
"policies": "root",
"auth_type": iamAuthType,
}
roleRequest := &logical.Request{
Operation: logical.CreateOperation,
Path: "role/" + testValidRoleName,
Storage: storage,
Data: roleData,
}
resp, err = b.HandleRequest(ctx, roleRequest)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err)
}
// configuring a valid role we won't be able to login to
roleDataEc2 := map[string]interface{}{
"auth_type": "ec2",
"policies": "root",
"bound_ami_id": "ami-1234567",
}
roleRequestEc2 := &logical.Request{
Operation: logical.CreateOperation,
Path: "role/ec2only",
Storage: storage,
Data: roleDataEc2,
}
resp, err = b.HandleRequest(ctx, roleRequestEc2)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: failed to create role; resp:%#v\nerr:%v", resp, err)
}
fakeArn := "arn:aws:iam::123456789012:role/somePath/FakeRole"
fakeArn2 := "arn:aws:iam::123456789012:role/somePath/FakeRole2"
fakeArnResolverCount := 0
fakeArnResolver := func(ctx context.Context, s logical.Storage, arn string) (string, error) {
if strings.HasPrefix(arn, fakeArn) {
fakeArnResolverCount++
return fmt.Sprintf("FakeUniqueIdFor%s%d", arn, fakeArnResolverCount), nil
}
return b.resolveArnToRealUniqueId(context.Background(), s, arn)
}
b.resolveArnToUniqueIDFunc = fakeArnResolver
// now we're creating the invalid role we won't be able to login to
roleData["bound_iam_principal_arn"] = []string{fakeArn, fakeArn2}
roleRequest.Path = "role/" + testInvalidRoleName
resp, err = b.HandleRequest(context.Background(), roleRequest)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: didn't fail to create role: resp:%#v\nerr:%v", resp, err)
}
// now, create the request without the signed header
stsRequestNoHeader, _ := stsService.GetCallerIdentityRequest(stsInputParams)
stsRequestNoHeader.Sign()
loginData, err := buildCallerIdentityLoginData(stsRequestNoHeader.HTTPRequest, testValidRoleName)
if err != nil {
t.Fatal(err)
}
loginRequest := &logical.Request{
Operation: logical.UpdateOperation,
Path: "login",
Storage: storage,
Data: loginData,
}
resp, err = b.HandleRequest(ctx, loginRequest)
if err != nil || resp == nil || !resp.IsError() {
t.Errorf("bad: expected failed login due to missing header: resp:%#v\nerr:%v", resp, err)
}
// create the request with the invalid header value
// Not reusing stsRequestNoHeader because the process of signing the request
// and reading the body modifies the underlying request, so it's just cleaner
// to get new requests.
stsRequestInvalidHeader, _ := stsService.GetCallerIdentityRequest(stsInputParams)
stsRequestInvalidHeader.HTTPRequest.Header.Add(iamServerIdHeader, "InvalidValue")
stsRequestInvalidHeader.Sign()
loginData, err = buildCallerIdentityLoginData(stsRequestInvalidHeader.HTTPRequest, testValidRoleName)
if err != nil {
t.Fatal(err)
}
loginRequest = &logical.Request{
Operation: logical.UpdateOperation,
Path: "login",
Storage: storage,
Data: loginData,
}
resp, err = b.HandleRequest(ctx, loginRequest)
if err != nil || resp == nil || !resp.IsError() {
t.Errorf("bad: expected failed login due to invalid header: resp:%#v\nerr:%v", resp, err)
}
// Now, valid request against invalid role
stsRequestValid, _ := stsService.GetCallerIdentityRequest(stsInputParams)
stsRequestValid.HTTPRequest.Header.Add(iamServerIdHeader, testVaultHeaderValue)
stsRequestValid.Sign()
loginData, err = buildCallerIdentityLoginData(stsRequestValid.HTTPRequest, testInvalidRoleName)
if err != nil {
t.Fatal(err)
}
loginRequest = &logical.Request{
Operation: logical.UpdateOperation,
Path: "login",
Storage: storage,
Data: loginData,
}
resp, err = b.HandleRequest(ctx, loginRequest)
if err != nil || resp == nil || !resp.IsError() {
t.Errorf("bad: expected failed login due to invalid role: resp:%#v\nerr:%v", resp, err)
}
loginData["role"] = "ec2only"
resp, err = b.HandleRequest(ctx, loginRequest)
if err != nil || resp == nil || !resp.IsError() {
t.Errorf("bad: expected failed login due to bad auth type: resp:%#v\nerr:%v", resp, err)
}
// finally, the happy path test :)
loginData["role"] = testValidRoleName
resp, err = b.HandleRequest(ctx, loginRequest)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Auth == nil || resp.IsError() {
t.Fatalf("bad: expected valid login: resp:%#v", resp)
}
if resp.Auth.Alias == nil {
t.Fatalf("bad: nil auth Alias")
}
if resp.Auth.Alias.Name != *testIdentity.Arn {
t.Fatalf("bad: expected identity alias of %q, got %q instead", *testIdentity.Arn, resp.Auth.Alias.Name)
}
renewReq := generateRenewRequest(storage, resp.Auth)
// dump a fake ARN into the metadata to ensure that we ONLY look
// at the unique ID that has been generated
renewReq.Auth.Metadata["canonical_arn"] = "fake_arn"
emptyLoginFd := &framework.FieldData{
Raw: map[string]interface{}{},
Schema: b.pathLogin().Fields,
}
// ensure we can renew
resp, err = b.pathLoginRenew(ctx, renewReq, emptyLoginFd)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("got nil response from renew")
}
if resp.IsError() {
t.Fatalf("got error when renewing: %#v", *resp)
}
// Now, fake out the unique ID resolver to ensure we fail login if the unique ID
// changes from under us
b.resolveArnToUniqueIDFunc = resolveArnToFakeUniqueId
// First, we need to update the role to force Vault to use our fake resolver to
// pick up the fake user ID
roleData["bound_iam_principal_arn"] = entity.canonicalArn()
roleRequest.Path = "role/" + testValidRoleName
resp, err = b.HandleRequest(ctx, roleRequest)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: failed to recreate role: resp:%#v\nerr:%v", resp, err)
}
resp, err = b.HandleRequest(ctx, loginRequest)
if err != nil || resp == nil || !resp.IsError() {
t.Errorf("bad: expected failed login due to changed AWS role ID: resp: %#v\nerr:%v", resp, err)
}
// and ensure a renew no longer works
resp, err = b.pathLoginRenew(ctx, renewReq, emptyLoginFd)
if err == nil || (resp != nil && !resp.IsError()) {
t.Errorf("bad: expected failed renew due to changed AWS role ID: resp: %#v", resp)
}
// Undo the fake resolver...
b.resolveArnToUniqueIDFunc = b.resolveArnToRealUniqueId
// Now test that wildcard matching works
wildcardRoleName := "valid_wildcard"
wildcardEntity := *entity
wildcardEntity.FriendlyName = "*"
roleData["bound_iam_principal_arn"] = []string{wildcardEntity.canonicalArn(), "arn:aws:iam::123456789012:role/DoesNotExist/Vault_Fake_Role*"}
roleRequest.Path = "role/" + wildcardRoleName
resp, err = b.HandleRequest(ctx, roleRequest)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: failed to create wildcard roles: resp:%#v\nerr:%v", resp, err)
}
loginData["role"] = wildcardRoleName
resp, err = b.HandleRequest(ctx, loginRequest)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Auth == nil || resp.IsError() {
t.Fatalf("bad: expected valid login: resp:%#v", resp)
}
// and ensure we can renew
renewReq = generateRenewRequest(storage, resp.Auth)
resp, err = b.pathLoginRenew(ctx, renewReq, emptyLoginFd)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("got nil response from renew")
}
if resp.IsError() {
t.Fatalf("got error when renewing: %#v", *resp)
}
// ensure the cache is populated
clientUserIDRaw, ok := resp.Auth.InternalData["client_user_id"]
if !ok {
t.Errorf("client_user_id not found in response")
}
clientUserID, ok := clientUserIDRaw.(string)
if !ok {
t.Errorf("client_user_id is not a string: %#v", clientUserIDRaw)
}
cachedArn := b.getCachedUserId(clientUserID)
if cachedArn == "" {
t.Errorf("got empty ARN back from user ID cache; expected full arn")
}
// Test for renewal with period
period := 600 * time.Second
roleData["period"] = period.String()
roleRequest.Path = "role/" + testValidRoleName
resp, err = b.HandleRequest(ctx, roleRequest)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: failed to create wildcard role: resp:%#v\nerr:%v", resp, err)
}
loginData["role"] = testValidRoleName
resp, err = b.HandleRequest(ctx, loginRequest)
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Auth == nil || resp.IsError() {
t.Fatalf("bad: expected valid login: resp:%#v", resp)
}
renewReq = generateRenewRequest(storage, resp.Auth)
resp, err = b.pathLoginRenew(context.Background(), renewReq, emptyLoginFd)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("got nil response from renew")
}
if resp.IsError() {
t.Fatalf("got error when renewing: %#v", *resp)
}
if resp.Auth.Period != period {
t.Fatalf("expected a period value of %s in the response, got: %s", period, resp.Auth.Period)
}
}
func generateRenewRequest(s logical.Storage, auth *logical.Auth) *logical.Request {
renewReq := &logical.Request{
Storage: s,
Auth: &logical.Auth{},
}
renewReq.Auth.InternalData = auth.InternalData
renewReq.Auth.Metadata = auth.Metadata
renewReq.Auth.LeaseOptions = auth.LeaseOptions
renewReq.Auth.Policies = auth.Policies
renewReq.Auth.Period = auth.Period
return renewReq
}
func TestGeneratePartitionToRegionMap(t *testing.T) {
m := generatePartitionToRegionMap()
if m["aws"].ID() != "us-east-1" {
t.Fatal("expected us-east-1 but received " + m["aws"].ID())
}
if m["aws-us-gov"].ID() != "us-gov-west-1" {
t.Fatal("expected us-gov-west-1 but received " + m["aws-us-gov"].ID())
}
}