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