| // 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()) |
| } |
| } |