// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package identitytpl import ( "errors" "fmt" "math" "strconv" "testing" "time" "github.com/hashicorp/vault/sdk/logical" ) // intentionally != time.Now() to catch latent used of time.Now instead of // passed in values var testNow = time.Now().Add(100 * time.Hour) func TestPopulate_Basic(t *testing.T) { tests := []struct { mode int name string input string output string err error entityName string metadata map[string]string aliasAccessor string aliasID string aliasName string nilEntity bool validityCheckOnly bool aliasMetadata map[string]string aliasCustomMetadata map[string]string groupName string groupMetadata map[string]string groupMemberships []string now time.Time }{ // time.* tests. Keep tests with time.Now() at the front to avoid false // positives due to the second changing during the test { name: "time now", input: "{{time.now}}", output: strconv.Itoa(int(testNow.Unix())), now: testNow, }, { name: "time plus", input: "{{time.now.plus.1h}}", output: strconv.Itoa(int(testNow.Unix() + (60 * 60))), now: testNow, }, { name: "time plus", input: "{{time.now.minus.5m}}", output: strconv.Itoa(int(testNow.Unix() - (5 * 60))), now: testNow, }, { name: "invalid operator", input: "{{time.now.divide.5m}}", err: errors.New("invalid time operator \"divide\""), }, { name: "time missing operand", input: "{{time.now.plus}}", err: errors.New("missing time operand"), }, { name: "no_templating", input: "path foobar {", output: "path foobar {", }, { name: "only_closing", input: "path foobar}} {", err: ErrUnbalancedTemplatingCharacter, }, { name: "closing_in_front", input: "path }} {{foobar}} {", err: ErrUnbalancedTemplatingCharacter, }, { name: "closing_in_back", input: "path {{foobar}} }}", err: ErrUnbalancedTemplatingCharacter, }, { name: "basic", input: "path /{{identity.entity.id}}/ {", output: "path /entityID/ {", }, { name: "multiple", input: "path {{identity.entity.name}} {\n\tval = {{identity.entity.metadata.foo}}\n}", entityName: "entityName", metadata: map[string]string{"foo": "bar"}, output: "path entityName {\n\tval = bar\n}", }, { name: "multiple_bad_name", input: "path {{identity.entity.name}} {\n\tval = {{identity.entity.metadata.foo}}\n}", metadata: map[string]string{"foo": "bar"}, err: ErrTemplateValueNotFound, }, { name: "unbalanced_close", input: "path {{identity.entity.id}} {\n\tval = {{ent}}ity.metadata.foo}}\n}", err: ErrUnbalancedTemplatingCharacter, }, { name: "unbalanced_open", input: "path {{identity.entity.id}} {\n\tval = {{ent{{ity.metadata.foo}}\n}", err: ErrUnbalancedTemplatingCharacter, }, { name: "no_entity_no_directives", input: "path {{identity.entity.id}} {\n\tval = {{ent{{ity.metadata.foo}}\n}", err: ErrNoEntityAttachedToToken, nilEntity: true, }, { name: "no_entity_no_diretives", input: "path name {\n\tval = foo\n}", output: "path name {\n\tval = foo\n}", nilEntity: true, }, { name: "alias_id_name", input: "path {{ identity.entity.name}} {\n\tval = {{identity.entity.aliases.foomount.id}} nval = {{identity.entity.aliases.foomount.name}}\n}", entityName: "entityName", aliasAccessor: "foomount", aliasID: "aliasID", aliasName: "aliasName", metadata: map[string]string{"foo": "bar"}, output: "path entityName {\n\tval = aliasID nval = aliasName\n}", }, { name: "alias_id_name_bad_selector", input: "path foobar {\n\tval = {{identity.entity.aliases.foomount}}\n}", aliasAccessor: "foomount", err: errors.New("invalid alias selector"), }, { name: "alias_id_name_bad_accessor", input: "path \"foobar\" {\n\tval = {{identity.entity.aliases.barmount.id}}\n}", aliasAccessor: "foomount", err: errors.New("alias not found"), }, { name: "alias_id_name", input: "path \"{{identity.entity.name}}\" {\n\tval = {{identity.entity.aliases.foomount.metadata.zip}}\n}", entityName: "entityName", aliasAccessor: "foomount", aliasID: "aliasID", metadata: map[string]string{"foo": "bar"}, aliasMetadata: map[string]string{"zip": "zap"}, output: "path \"entityName\" {\n\tval = zap\n}", }, { name: "group_name", input: "path \"{{identity.groups.ids.groupID.name}}\" {\n\tval = {{identity.entity.name}}\n}", entityName: "entityName", groupName: "groupName", output: "path \"groupName\" {\n\tval = entityName\n}", }, { name: "group_bad_id", input: "path \"{{identity.groups.ids.hroupID.name}}\" {\n\tval = {{identity.entity.name}}\n}", entityName: "entityName", groupName: "groupName", err: errors.New("entity is not a member of group \"hroupID\""), }, { name: "group_id", input: "path \"{{identity.groups.names.groupName.id}}\" {\n\tval = {{identity.entity.name}}\n}", entityName: "entityName", groupName: "groupName", output: "path \"groupID\" {\n\tval = entityName\n}", }, { name: "group_bad_name", input: "path \"{{identity.groups.names.hroupName.id}}\" {\n\tval = {{identity.entity.name}}\n}", entityName: "entityName", groupName: "groupName", err: errors.New("entity is not a member of group \"hroupName\""), }, { name: "metadata_object_disallowed", input: "{{identity.entity.metadata}}", metadata: map[string]string{"foo": "bar"}, err: ErrTemplateValueNotFound, }, { name: "alias_metadata_object_disallowed", input: "{{identity.entity.aliases.foomount.metadata}}", aliasAccessor: "foomount", aliasMetadata: map[string]string{"foo": "bar"}, err: ErrTemplateValueNotFound, }, { name: "groups.names_disallowed", input: "{{identity.entity.groups.names}}", groupMemberships: []string{"foo", "bar"}, err: ErrTemplateValueNotFound, }, { name: "groups.ids_disallowed", input: "{{identity.entity.groups.ids}}", groupMemberships: []string{"foo", "bar"}, err: ErrTemplateValueNotFound, }, // missing selector cases { mode: JSONTemplating, name: "entity id", input: "{{identity.entity.id}}", output: `"entityID"`, }, { mode: JSONTemplating, name: "entity name", input: "{{identity.entity.name}}", entityName: "entityName", output: `"entityName"`, }, { mode: JSONTemplating, name: "entity name missing", input: "{{identity.entity.name}}", output: `""`, }, { mode: JSONTemplating, name: "alias name/id", input: "{{identity.entity.aliases.foomount.id}} {{identity.entity.aliases.foomount.name}}", aliasAccessor: "foomount", aliasID: "aliasID", aliasName: "aliasName", output: `"aliasID" "aliasName"`, }, { mode: JSONTemplating, name: "one metadata key", input: "{{identity.entity.metadata.color}}", metadata: map[string]string{"foo": "bar", "color": "green"}, output: `"green"`, }, { mode: JSONTemplating, name: "one metadata key not found", input: "{{identity.entity.metadata.size}}", metadata: map[string]string{"foo": "bar", "color": "green"}, output: `""`, }, { mode: JSONTemplating, name: "all entity metadata", input: "{{identity.entity.metadata}}", metadata: map[string]string{"foo": "bar", "color": "green"}, output: `{"color":"green","foo":"bar"}`, }, { mode: JSONTemplating, name: "null entity metadata", input: "{{identity.entity.metadata}}", output: `{}`, }, { mode: JSONTemplating, name: "groups.names", input: "{{identity.entity.groups.names}}", groupMemberships: []string{"foo", "bar"}, output: `["foo","bar"]`, }, { mode: JSONTemplating, name: "groups.ids", input: "{{identity.entity.groups.ids}}", groupMemberships: []string{"foo", "bar"}, output: `["foo_0","bar_1"]`, }, { mode: JSONTemplating, name: "one alias metadata key", input: "{{identity.entity.aliases.aws_123.metadata.color}}", aliasAccessor: "aws_123", aliasMetadata: map[string]string{"foo": "bar", "color": "green"}, output: `"green"`, }, { mode: JSONTemplating, name: "one alias metadata key not found", input: "{{identity.entity.aliases.aws_123.metadata.size}}", aliasAccessor: "aws_123", aliasMetadata: map[string]string{"foo": "bar", "color": "green"}, output: `""`, }, { mode: JSONTemplating, name: "one alias metadata, accessor not found", input: "{{identity.entity.aliases.aws_123.metadata.size}}", aliasAccessor: "not_gonna_match", aliasMetadata: map[string]string{"foo": "bar", "color": "green"}, output: `""`, }, { mode: JSONTemplating, name: "all alias metadata", input: "{{identity.entity.aliases.aws_123.metadata}}", aliasAccessor: "aws_123", aliasMetadata: map[string]string{"foo": "bar", "color": "green"}, output: `{"color":"green","foo":"bar"}`, }, { mode: JSONTemplating, name: "null alias metadata", input: "{{identity.entity.aliases.aws_123.metadata}}", aliasAccessor: "aws_123", output: `{}`, }, { mode: JSONTemplating, name: "all alias metadata, accessor not found", input: "{{identity.entity.aliases.aws_123.metadata}}", aliasAccessor: "not_gonna_match", aliasMetadata: map[string]string{"foo": "bar", "color": "green"}, output: `{}`, }, { mode: JSONTemplating, name: "one alias custom metadata key", input: "{{identity.entity.aliases.aws_123.custom_metadata.foo}}", aliasAccessor: "aws_123", aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"}, output: `"abc"`, }, { mode: JSONTemplating, name: "one alias custom metadata key not found", input: "{{identity.entity.aliases.aws_123.custom_metadata.size}}", aliasAccessor: "aws_123", aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"}, output: `""`, }, { mode: JSONTemplating, name: "one alias custom metadata, accessor not found", input: "{{identity.entity.aliases.aws_123.custom_metadata.size}}", aliasAccessor: "not_gonna_match", aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"}, output: `""`, }, { mode: JSONTemplating, name: "all alias custom metadata", input: "{{identity.entity.aliases.aws_123.custom_metadata}}", aliasAccessor: "aws_123", aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"}, output: `{"bar":"123","foo":"abc"}`, }, { mode: JSONTemplating, name: "null alias custom metadata", input: "{{identity.entity.aliases.aws_123.custom_metadata}}", aliasAccessor: "aws_123", output: `{}`, }, { mode: JSONTemplating, name: "all alias custom metadata, accessor not found", input: "{{identity.entity.aliases.aws_123.custom_metadata}}", aliasAccessor: "not_gonna_match", aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"}, output: `{}`, }, } for _, test := range tests { var entity *logical.Entity if !test.nilEntity { entity = &logical.Entity{ ID: "entityID", Name: test.entityName, Metadata: test.metadata, } } if test.aliasAccessor != "" { entity.Aliases = []*logical.Alias{ { MountAccessor: test.aliasAccessor, ID: test.aliasID, Name: test.aliasName, Metadata: test.aliasMetadata, CustomMetadata: test.aliasCustomMetadata, }, } } var groups []*logical.Group if test.groupName != "" { groups = append(groups, &logical.Group{ ID: "groupID", Name: test.groupName, Metadata: test.groupMetadata, NamespaceID: "root", }) } if test.groupMemberships != nil { for i, groupName := range test.groupMemberships { groups = append(groups, &logical.Group{ ID: fmt.Sprintf("%s_%d", groupName, i), Name: groupName, }) } } subst, out, err := PopulateString(PopulateStringInput{ Mode: test.mode, ValidityCheckOnly: test.validityCheckOnly, String: test.input, Entity: entity, Groups: groups, NamespaceID: "root", Now: test.now, }) if err != nil { if test.err == nil { t.Fatalf("%s: expected success, got error: %v", test.name, err) } if err.Error() != test.err.Error() { t.Fatalf("%s: got error: %v", test.name, err) } } if out != test.output { t.Fatalf("%s: bad output: %s, expected: %s", test.name, out, test.output) } if err == nil && !subst && out != test.input { t.Fatalf("%s: bad subst flag", test.name) } } } func TestPopulate_CurrentTime(t *testing.T) { now := time.Now() // Test that an unset Now parameter results in current time input := PopulateStringInput{ Mode: JSONTemplating, String: `{{time.now}}`, } _, out, err := PopulateString(input) if err != nil { t.Fatal(err) } nowPopulated, err := strconv.Atoi(out) if err != nil { t.Fatal(err) } diff := math.Abs(float64(int64(nowPopulated) - now.Unix())) if diff > 1 { t.Fatalf("expected time within 1 second. Got diff of: %f", diff) } } func TestPopulate_FullObject(t *testing.T) { testEntity := &logical.Entity{ ID: "abc-123", Name: "Entity Name", Metadata: map[string]string{ "color": "green", "size": "small", "non-printable": "\"\n\t", }, Aliases: []*logical.Alias{ { MountAccessor: "aws_123", Metadata: map[string]string{ "service": "ec2", "region": "west", }, CustomMetadata: map[string]string{ "foo": "abc", "bar": "123", }, }, }, } testGroups := []*logical.Group{ {ID: "a08b0c02", Name: "g1"}, {ID: "239bef91", Name: "g2"}, } template := ` { "id": {{identity.entity.id}}, "name": {{identity.entity.name}}, "all metadata": {{identity.entity.metadata}}, "one metadata key": {{identity.entity.metadata.color}}, "one metadata key not found": {{identity.entity.metadata.asldfk}}, "alias metadata": {{identity.entity.aliases.aws_123.metadata}}, "alias not found metadata": {{identity.entity.aliases.blahblah.metadata}}, "one alias metadata key": {{identity.entity.aliases.aws_123.metadata.service}}, "one not found alias metadata key": {{identity.entity.aliases.blahblah.metadata.service}}, "group names": {{identity.entity.groups.names}}, "group ids": {{identity.entity.groups.ids}}, "repeated and": {"nested element": {{identity.entity.name}}}, "alias custom metadata": {{identity.entity.aliases.aws_123.custom_metadata}}, "alias not found custom metadata": {{identity.entity.aliases.blahblah.custom_metadata}}, "one alias custom metadata key": {{identity.entity.aliases.aws_123.custom_metadata.foo}}, "one not found alias custom metadata key": {{identity.entity.aliases.blahblah.custom_metadata.foo}}, }` expected := ` { "id": "abc-123", "name": "Entity Name", "all metadata": {"color":"green","non-printable":"\"\n\t","size":"small"}, "one metadata key": "green", "one metadata key not found": "", "alias metadata": {"region":"west","service":"ec2"}, "alias not found metadata": {}, "one alias metadata key": "ec2", "one not found alias metadata key": "", "group names": ["g1","g2"], "group ids": ["a08b0c02","239bef91"], "repeated and": {"nested element": "Entity Name"}, "alias custom metadata": {"bar":"123","foo":"abc"}, "alias not found custom metadata": {}, "one alias custom metadata key": "abc", "one not found alias custom metadata key": "", }` input := PopulateStringInput{ Mode: JSONTemplating, String: template, Entity: testEntity, Groups: testGroups, } _, out, err := PopulateString(input) if err != nil { t.Fatal(err) } if out != expected { t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, out) } }