github.com/hashicorp/vault/sdk@v0.11.0/helper/identitytpl/templating_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package identitytpl
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"math"
    10  	"strconv"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/hashicorp/vault/sdk/logical"
    15  )
    16  
    17  // intentionally != time.Now() to catch latent used of time.Now instead of
    18  // passed in values
    19  var testNow = time.Now().Add(100 * time.Hour)
    20  
    21  func TestPopulate_Basic(t *testing.T) {
    22  	tests := []struct {
    23  		mode                int
    24  		name                string
    25  		input               string
    26  		output              string
    27  		err                 error
    28  		entityName          string
    29  		metadata            map[string]string
    30  		aliasAccessor       string
    31  		aliasID             string
    32  		aliasName           string
    33  		nilEntity           bool
    34  		validityCheckOnly   bool
    35  		aliasMetadata       map[string]string
    36  		aliasCustomMetadata map[string]string
    37  		groupName           string
    38  		groupMetadata       map[string]string
    39  		groupMemberships    []string
    40  		now                 time.Time
    41  	}{
    42  		// time.* tests. Keep tests with time.Now() at the front to avoid false
    43  		// positives due to the second changing during the test
    44  		{
    45  			name:   "time now",
    46  			input:  "{{time.now}}",
    47  			output: strconv.Itoa(int(testNow.Unix())),
    48  			now:    testNow,
    49  		},
    50  		{
    51  			name:   "time plus",
    52  			input:  "{{time.now.plus.1h}}",
    53  			output: strconv.Itoa(int(testNow.Unix() + (60 * 60))),
    54  			now:    testNow,
    55  		},
    56  		{
    57  			name:   "time plus",
    58  			input:  "{{time.now.minus.5m}}",
    59  			output: strconv.Itoa(int(testNow.Unix() - (5 * 60))),
    60  			now:    testNow,
    61  		},
    62  		{
    63  			name:  "invalid operator",
    64  			input: "{{time.now.divide.5m}}",
    65  			err:   errors.New("invalid time operator \"divide\""),
    66  		},
    67  		{
    68  			name:  "time missing operand",
    69  			input: "{{time.now.plus}}",
    70  			err:   errors.New("missing time operand"),
    71  		},
    72  
    73  		{
    74  			name:   "no_templating",
    75  			input:  "path foobar {",
    76  			output: "path foobar {",
    77  		},
    78  		{
    79  			name:  "only_closing",
    80  			input: "path foobar}} {",
    81  			err:   ErrUnbalancedTemplatingCharacter,
    82  		},
    83  		{
    84  			name:  "closing_in_front",
    85  			input: "path }} {{foobar}} {",
    86  			err:   ErrUnbalancedTemplatingCharacter,
    87  		},
    88  		{
    89  			name:  "closing_in_back",
    90  			input: "path {{foobar}} }}",
    91  			err:   ErrUnbalancedTemplatingCharacter,
    92  		},
    93  		{
    94  			name:   "basic",
    95  			input:  "path /{{identity.entity.id}}/ {",
    96  			output: "path /entityID/ {",
    97  		},
    98  		{
    99  			name:       "multiple",
   100  			input:      "path {{identity.entity.name}} {\n\tval = {{identity.entity.metadata.foo}}\n}",
   101  			entityName: "entityName",
   102  			metadata:   map[string]string{"foo": "bar"},
   103  			output:     "path entityName {\n\tval = bar\n}",
   104  		},
   105  		{
   106  			name:     "multiple_bad_name",
   107  			input:    "path {{identity.entity.name}} {\n\tval = {{identity.entity.metadata.foo}}\n}",
   108  			metadata: map[string]string{"foo": "bar"},
   109  			err:      ErrTemplateValueNotFound,
   110  		},
   111  		{
   112  			name:  "unbalanced_close",
   113  			input: "path {{identity.entity.id}} {\n\tval = {{ent}}ity.metadata.foo}}\n}",
   114  			err:   ErrUnbalancedTemplatingCharacter,
   115  		},
   116  		{
   117  			name:  "unbalanced_open",
   118  			input: "path {{identity.entity.id}} {\n\tval = {{ent{{ity.metadata.foo}}\n}",
   119  			err:   ErrUnbalancedTemplatingCharacter,
   120  		},
   121  		{
   122  			name:      "no_entity_no_directives",
   123  			input:     "path {{identity.entity.id}} {\n\tval = {{ent{{ity.metadata.foo}}\n}",
   124  			err:       ErrNoEntityAttachedToToken,
   125  			nilEntity: true,
   126  		},
   127  		{
   128  			name:      "no_entity_no_diretives",
   129  			input:     "path name {\n\tval = foo\n}",
   130  			output:    "path name {\n\tval = foo\n}",
   131  			nilEntity: true,
   132  		},
   133  		{
   134  			name:          "alias_id_name",
   135  			input:         "path {{ identity.entity.name}} {\n\tval = {{identity.entity.aliases.foomount.id}}  nval = {{identity.entity.aliases.foomount.name}}\n}",
   136  			entityName:    "entityName",
   137  			aliasAccessor: "foomount",
   138  			aliasID:       "aliasID",
   139  			aliasName:     "aliasName",
   140  			metadata:      map[string]string{"foo": "bar"},
   141  			output:        "path entityName {\n\tval = aliasID  nval = aliasName\n}",
   142  		},
   143  		{
   144  			name:          "alias_id_name_bad_selector",
   145  			input:         "path foobar {\n\tval = {{identity.entity.aliases.foomount}}\n}",
   146  			aliasAccessor: "foomount",
   147  			err:           errors.New("invalid alias selector"),
   148  		},
   149  		{
   150  			name:          "alias_id_name_bad_accessor",
   151  			input:         "path \"foobar\" {\n\tval = {{identity.entity.aliases.barmount.id}}\n}",
   152  			aliasAccessor: "foomount",
   153  			err:           errors.New("alias not found"),
   154  		},
   155  		{
   156  			name:          "alias_id_name",
   157  			input:         "path \"{{identity.entity.name}}\" {\n\tval = {{identity.entity.aliases.foomount.metadata.zip}}\n}",
   158  			entityName:    "entityName",
   159  			aliasAccessor: "foomount",
   160  			aliasID:       "aliasID",
   161  			metadata:      map[string]string{"foo": "bar"},
   162  			aliasMetadata: map[string]string{"zip": "zap"},
   163  			output:        "path \"entityName\" {\n\tval = zap\n}",
   164  		},
   165  		{
   166  			name:       "group_name",
   167  			input:      "path \"{{identity.groups.ids.groupID.name}}\" {\n\tval = {{identity.entity.name}}\n}",
   168  			entityName: "entityName",
   169  			groupName:  "groupName",
   170  			output:     "path \"groupName\" {\n\tval = entityName\n}",
   171  		},
   172  		{
   173  			name:       "group_bad_id",
   174  			input:      "path \"{{identity.groups.ids.hroupID.name}}\" {\n\tval = {{identity.entity.name}}\n}",
   175  			entityName: "entityName",
   176  			groupName:  "groupName",
   177  			err:        errors.New("entity is not a member of group \"hroupID\""),
   178  		},
   179  		{
   180  			name:       "group_id",
   181  			input:      "path \"{{identity.groups.names.groupName.id}}\" {\n\tval = {{identity.entity.name}}\n}",
   182  			entityName: "entityName",
   183  			groupName:  "groupName",
   184  			output:     "path \"groupID\" {\n\tval = entityName\n}",
   185  		},
   186  		{
   187  			name:       "group_bad_name",
   188  			input:      "path \"{{identity.groups.names.hroupName.id}}\" {\n\tval = {{identity.entity.name}}\n}",
   189  			entityName: "entityName",
   190  			groupName:  "groupName",
   191  			err:        errors.New("entity is not a member of group \"hroupName\""),
   192  		},
   193  		{
   194  			name:     "metadata_object_disallowed",
   195  			input:    "{{identity.entity.metadata}}",
   196  			metadata: map[string]string{"foo": "bar"},
   197  			err:      ErrTemplateValueNotFound,
   198  		},
   199  		{
   200  			name:          "alias_metadata_object_disallowed",
   201  			input:         "{{identity.entity.aliases.foomount.metadata}}",
   202  			aliasAccessor: "foomount",
   203  			aliasMetadata: map[string]string{"foo": "bar"},
   204  			err:           ErrTemplateValueNotFound,
   205  		},
   206  		{
   207  			name:             "groups.names_disallowed",
   208  			input:            "{{identity.entity.groups.names}}",
   209  			groupMemberships: []string{"foo", "bar"},
   210  			err:              ErrTemplateValueNotFound,
   211  		},
   212  		{
   213  			name:             "groups.ids_disallowed",
   214  			input:            "{{identity.entity.groups.ids}}",
   215  			groupMemberships: []string{"foo", "bar"},
   216  			err:              ErrTemplateValueNotFound,
   217  		},
   218  
   219  		// missing selector cases
   220  		{
   221  			mode:   JSONTemplating,
   222  			name:   "entity id",
   223  			input:  "{{identity.entity.id}}",
   224  			output: `"entityID"`,
   225  		},
   226  		{
   227  			mode:       JSONTemplating,
   228  			name:       "entity name",
   229  			input:      "{{identity.entity.name}}",
   230  			entityName: "entityName",
   231  			output:     `"entityName"`,
   232  		},
   233  		{
   234  			mode:   JSONTemplating,
   235  			name:   "entity name missing",
   236  			input:  "{{identity.entity.name}}",
   237  			output: `""`,
   238  		},
   239  		{
   240  			mode:          JSONTemplating,
   241  			name:          "alias name/id",
   242  			input:         "{{identity.entity.aliases.foomount.id}} {{identity.entity.aliases.foomount.name}}",
   243  			aliasAccessor: "foomount",
   244  			aliasID:       "aliasID",
   245  			aliasName:     "aliasName",
   246  			output:        `"aliasID" "aliasName"`,
   247  		},
   248  		{
   249  			mode:     JSONTemplating,
   250  			name:     "one metadata key",
   251  			input:    "{{identity.entity.metadata.color}}",
   252  			metadata: map[string]string{"foo": "bar", "color": "green"},
   253  			output:   `"green"`,
   254  		},
   255  		{
   256  			mode:     JSONTemplating,
   257  			name:     "one metadata key not found",
   258  			input:    "{{identity.entity.metadata.size}}",
   259  			metadata: map[string]string{"foo": "bar", "color": "green"},
   260  			output:   `""`,
   261  		},
   262  		{
   263  			mode:     JSONTemplating,
   264  			name:     "all entity metadata",
   265  			input:    "{{identity.entity.metadata}}",
   266  			metadata: map[string]string{"foo": "bar", "color": "green"},
   267  			output:   `{"color":"green","foo":"bar"}`,
   268  		},
   269  		{
   270  			mode:   JSONTemplating,
   271  			name:   "null entity metadata",
   272  			input:  "{{identity.entity.metadata}}",
   273  			output: `{}`,
   274  		},
   275  		{
   276  			mode:             JSONTemplating,
   277  			name:             "groups.names",
   278  			input:            "{{identity.entity.groups.names}}",
   279  			groupMemberships: []string{"foo", "bar"},
   280  			output:           `["foo","bar"]`,
   281  		},
   282  		{
   283  			mode:             JSONTemplating,
   284  			name:             "groups.ids",
   285  			input:            "{{identity.entity.groups.ids}}",
   286  			groupMemberships: []string{"foo", "bar"},
   287  			output:           `["foo_0","bar_1"]`,
   288  		},
   289  		{
   290  			mode:          JSONTemplating,
   291  			name:          "one alias metadata key",
   292  			input:         "{{identity.entity.aliases.aws_123.metadata.color}}",
   293  			aliasAccessor: "aws_123",
   294  			aliasMetadata: map[string]string{"foo": "bar", "color": "green"},
   295  			output:        `"green"`,
   296  		},
   297  		{
   298  			mode:          JSONTemplating,
   299  			name:          "one alias metadata key not found",
   300  			input:         "{{identity.entity.aliases.aws_123.metadata.size}}",
   301  			aliasAccessor: "aws_123",
   302  			aliasMetadata: map[string]string{"foo": "bar", "color": "green"},
   303  			output:        `""`,
   304  		},
   305  		{
   306  			mode:          JSONTemplating,
   307  			name:          "one alias metadata, accessor not found",
   308  			input:         "{{identity.entity.aliases.aws_123.metadata.size}}",
   309  			aliasAccessor: "not_gonna_match",
   310  			aliasMetadata: map[string]string{"foo": "bar", "color": "green"},
   311  			output:        `""`,
   312  		},
   313  		{
   314  			mode:          JSONTemplating,
   315  			name:          "all alias metadata",
   316  			input:         "{{identity.entity.aliases.aws_123.metadata}}",
   317  			aliasAccessor: "aws_123",
   318  			aliasMetadata: map[string]string{"foo": "bar", "color": "green"},
   319  			output:        `{"color":"green","foo":"bar"}`,
   320  		},
   321  		{
   322  			mode:          JSONTemplating,
   323  			name:          "null alias metadata",
   324  			input:         "{{identity.entity.aliases.aws_123.metadata}}",
   325  			aliasAccessor: "aws_123",
   326  			output:        `{}`,
   327  		},
   328  		{
   329  			mode:          JSONTemplating,
   330  			name:          "all alias metadata, accessor not found",
   331  			input:         "{{identity.entity.aliases.aws_123.metadata}}",
   332  			aliasAccessor: "not_gonna_match",
   333  			aliasMetadata: map[string]string{"foo": "bar", "color": "green"},
   334  			output:        `{}`,
   335  		},
   336  		{
   337  			mode:                JSONTemplating,
   338  			name:                "one alias custom metadata key",
   339  			input:               "{{identity.entity.aliases.aws_123.custom_metadata.foo}}",
   340  			aliasAccessor:       "aws_123",
   341  			aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"},
   342  			output:              `"abc"`,
   343  		},
   344  		{
   345  			mode:                JSONTemplating,
   346  			name:                "one alias custom metadata key not found",
   347  			input:               "{{identity.entity.aliases.aws_123.custom_metadata.size}}",
   348  			aliasAccessor:       "aws_123",
   349  			aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"},
   350  			output:              `""`,
   351  		},
   352  		{
   353  			mode:                JSONTemplating,
   354  			name:                "one alias custom metadata, accessor not found",
   355  			input:               "{{identity.entity.aliases.aws_123.custom_metadata.size}}",
   356  			aliasAccessor:       "not_gonna_match",
   357  			aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"},
   358  			output:              `""`,
   359  		},
   360  		{
   361  			mode:                JSONTemplating,
   362  			name:                "all alias custom metadata",
   363  			input:               "{{identity.entity.aliases.aws_123.custom_metadata}}",
   364  			aliasAccessor:       "aws_123",
   365  			aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"},
   366  			output:              `{"bar":"123","foo":"abc"}`,
   367  		},
   368  		{
   369  			mode:          JSONTemplating,
   370  			name:          "null alias custom metadata",
   371  			input:         "{{identity.entity.aliases.aws_123.custom_metadata}}",
   372  			aliasAccessor: "aws_123",
   373  			output:        `{}`,
   374  		},
   375  		{
   376  			mode:                JSONTemplating,
   377  			name:                "all alias custom metadata, accessor not found",
   378  			input:               "{{identity.entity.aliases.aws_123.custom_metadata}}",
   379  			aliasAccessor:       "not_gonna_match",
   380  			aliasCustomMetadata: map[string]string{"foo": "abc", "bar": "123"},
   381  			output:              `{}`,
   382  		},
   383  	}
   384  
   385  	for _, test := range tests {
   386  		var entity *logical.Entity
   387  		if !test.nilEntity {
   388  			entity = &logical.Entity{
   389  				ID:       "entityID",
   390  				Name:     test.entityName,
   391  				Metadata: test.metadata,
   392  			}
   393  		}
   394  		if test.aliasAccessor != "" {
   395  			entity.Aliases = []*logical.Alias{
   396  				{
   397  					MountAccessor:  test.aliasAccessor,
   398  					ID:             test.aliasID,
   399  					Name:           test.aliasName,
   400  					Metadata:       test.aliasMetadata,
   401  					CustomMetadata: test.aliasCustomMetadata,
   402  				},
   403  			}
   404  		}
   405  		var groups []*logical.Group
   406  		if test.groupName != "" {
   407  			groups = append(groups, &logical.Group{
   408  				ID:          "groupID",
   409  				Name:        test.groupName,
   410  				Metadata:    test.groupMetadata,
   411  				NamespaceID: "root",
   412  			})
   413  		}
   414  
   415  		if test.groupMemberships != nil {
   416  			for i, groupName := range test.groupMemberships {
   417  				groups = append(groups, &logical.Group{
   418  					ID:   fmt.Sprintf("%s_%d", groupName, i),
   419  					Name: groupName,
   420  				})
   421  			}
   422  		}
   423  
   424  		subst, out, err := PopulateString(PopulateStringInput{
   425  			Mode:              test.mode,
   426  			ValidityCheckOnly: test.validityCheckOnly,
   427  			String:            test.input,
   428  			Entity:            entity,
   429  			Groups:            groups,
   430  			NamespaceID:       "root",
   431  			Now:               test.now,
   432  		})
   433  		if err != nil {
   434  			if test.err == nil {
   435  				t.Fatalf("%s: expected success, got error: %v", test.name, err)
   436  			}
   437  			if err.Error() != test.err.Error() {
   438  				t.Fatalf("%s: got error: %v", test.name, err)
   439  			}
   440  		}
   441  		if out != test.output {
   442  			t.Fatalf("%s: bad output: %s, expected: %s", test.name, out, test.output)
   443  		}
   444  		if err == nil && !subst && out != test.input {
   445  			t.Fatalf("%s: bad subst flag", test.name)
   446  		}
   447  	}
   448  }
   449  
   450  func TestPopulate_CurrentTime(t *testing.T) {
   451  	now := time.Now()
   452  
   453  	// Test that an unset Now parameter results in current time
   454  	input := PopulateStringInput{
   455  		Mode:   JSONTemplating,
   456  		String: `{{time.now}}`,
   457  	}
   458  
   459  	_, out, err := PopulateString(input)
   460  	if err != nil {
   461  		t.Fatal(err)
   462  	}
   463  
   464  	nowPopulated, err := strconv.Atoi(out)
   465  	if err != nil {
   466  		t.Fatal(err)
   467  	}
   468  
   469  	diff := math.Abs(float64(int64(nowPopulated) - now.Unix()))
   470  	if diff > 1 {
   471  		t.Fatalf("expected time within 1 second. Got diff of: %f", diff)
   472  	}
   473  }
   474  
   475  func TestPopulate_FullObject(t *testing.T) {
   476  	testEntity := &logical.Entity{
   477  		ID:   "abc-123",
   478  		Name: "Entity Name",
   479  		Metadata: map[string]string{
   480  			"color":         "green",
   481  			"size":          "small",
   482  			"non-printable": "\"\n\t",
   483  		},
   484  		Aliases: []*logical.Alias{
   485  			{
   486  				MountAccessor: "aws_123",
   487  				Metadata: map[string]string{
   488  					"service": "ec2",
   489  					"region":  "west",
   490  				},
   491  				CustomMetadata: map[string]string{
   492  					"foo": "abc",
   493  					"bar": "123",
   494  				},
   495  			},
   496  		},
   497  	}
   498  
   499  	testGroups := []*logical.Group{
   500  		{ID: "a08b0c02", Name: "g1"},
   501  		{ID: "239bef91", Name: "g2"},
   502  	}
   503  
   504  	template := `
   505  			{
   506  			    "id": {{identity.entity.id}},
   507  			    "name": {{identity.entity.name}},
   508  			    "all metadata": {{identity.entity.metadata}},
   509  			    "one metadata key": {{identity.entity.metadata.color}},
   510  			    "one metadata key not found": {{identity.entity.metadata.asldfk}},
   511  			    "alias metadata": {{identity.entity.aliases.aws_123.metadata}},
   512  			    "alias not found metadata": {{identity.entity.aliases.blahblah.metadata}},
   513  			    "one alias metadata key": {{identity.entity.aliases.aws_123.metadata.service}},
   514  			    "one not found alias metadata key": {{identity.entity.aliases.blahblah.metadata.service}},
   515  			    "group names": {{identity.entity.groups.names}},
   516  			    "group ids": {{identity.entity.groups.ids}},
   517  			    "repeated and": {"nested element": {{identity.entity.name}}},
   518  				"alias custom metadata": {{identity.entity.aliases.aws_123.custom_metadata}},
   519  				"alias not found custom metadata": {{identity.entity.aliases.blahblah.custom_metadata}},
   520  				"one alias custom metadata key": {{identity.entity.aliases.aws_123.custom_metadata.foo}},
   521  				"one not found alias custom metadata key": {{identity.entity.aliases.blahblah.custom_metadata.foo}},
   522  			}`
   523  
   524  	expected := `
   525  			{
   526  			    "id": "abc-123",
   527  			    "name": "Entity Name",
   528  			    "all metadata": {"color":"green","non-printable":"\"\n\t","size":"small"},
   529  			    "one metadata key": "green",
   530  			    "one metadata key not found": "",
   531  			    "alias metadata": {"region":"west","service":"ec2"},
   532  			    "alias not found metadata": {},
   533  			    "one alias metadata key": "ec2",
   534  			    "one not found alias metadata key": "",
   535  			    "group names": ["g1","g2"],
   536  			    "group ids": ["a08b0c02","239bef91"],
   537  			    "repeated and": {"nested element": "Entity Name"},
   538  				"alias custom metadata": {"bar":"123","foo":"abc"},
   539  				"alias not found custom metadata": {},
   540  				"one alias custom metadata key": "abc",
   541  				"one not found alias custom metadata key": "",
   542  			}`
   543  
   544  	input := PopulateStringInput{
   545  		Mode:   JSONTemplating,
   546  		String: template,
   547  		Entity: testEntity,
   548  		Groups: testGroups,
   549  	}
   550  	_, out, err := PopulateString(input)
   551  	if err != nil {
   552  		t.Fatal(err)
   553  	}
   554  
   555  	if out != expected {
   556  		t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, out)
   557  	}
   558  }