From 4542eef22b14e3bf7b2a982c2eac48b962593ad0 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 15 Mar 2024 10:33:10 -0400 Subject: [PATCH] Add templating coalesce function (#4492) --- CHANGELOG.md | 6 ++++++ internal/credential/vault/private_library_test.go | 11 +++++++++++ internal/daemon/controller/auth/auth.go | 3 +++ internal/util/template/funcs.go | 11 +++++++++++ internal/util/template/generate.go | 1 + internal/util/template/generate_test.go | 6 +++++- internal/util/template/types.go | 9 +++++---- .../concepts/domain-model/credential-libraries.mdx | 13 ++++++++++--- 8 files changed, 52 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d61ada82df..28fec7ebcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. the alias value. Crate an alias with `boundary aliases create target -value example.boundary -destination-id ttcp_1234567890` and connect to a target using an alias using `boundary connect example.boundary` +* templating: A new templating function `coalesce` can be used to match a + template against multiple possible values, returning the first non-empty + value. As an example, this can be used in a credential library to allow a + username value that might be comprised of a name or login name depending on + the auth method, e.g. `{{ coalesce .Account.Name .Account.LoginName}}` + ([PR](https://github.com/hashicorp/boundary/pull/4492))) ### Added dependency diff --git a/internal/credential/vault/private_library_test.go b/internal/credential/vault/private_library_test.go index 0cbebe0f37..b2a3fd9a7b 100644 --- a/internal/credential/vault/private_library_test.go +++ b/internal/credential/vault/private_library_test.go @@ -1345,6 +1345,17 @@ func TestRepository_sshCertIssuingCredentialLibrary_retrieveCredential(t *testin opts: []Option{WithKeyType(KeyTypeEcdsa), WithKeyBits(256)}, retOpts: []credential.Option{credential.WithTemplateData(template.Data{Account: template.Account{Email: util.Pointer("rise-of-the-template@foobar.com")}})}, }, + { + name: "vault issue ec(256) cert with coalesce username", + username: `{{coalesce .Account.LoginName .Account.Name .Account.Email}}`, + expected: map[string]any{ + "username": "name-that-name", + "valid_principals": []string{"name-that-name"}, + }, + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeEcdsa), WithKeyBits(256)}, + retOpts: []credential.Option{credential.WithTemplateData(template.Data{Account: template.Account{Name: util.Pointer("name-that-name"), LoginName: util.Pointer(""), Email: util.Pointer("rise-of-the-template@foobar.com")}})}, + }, { name: "vault issue ec(384) cert with disallowed extension", username: "username-10-because-789", diff --git a/internal/daemon/controller/auth/auth.go b/internal/daemon/controller/auth/auth.go index 7689ce5cb2..a9054fe9bb 100644 --- a/internal/daemon/controller/auth/auth.go +++ b/internal/daemon/controller/auth/auth.go @@ -10,6 +10,7 @@ import ( "fmt" "hash" "hash/fnv" + "log" "net/http" "slices" "strings" @@ -599,8 +600,10 @@ func (v verifier) performAuthCheck(ctx context.Context) ( return } userData.Account.Name = util.Pointer(acct.GetName()) + log.Println("account name", *userData.Account.Name) userData.Account.Email = util.Pointer(acct.GetEmail()) userData.Account.LoginName = util.Pointer(acct.GetLoginName()) + log.Println("account login name", *userData.Account.LoginName) userData.Account.Subject = util.Pointer(acct.GetSubject()) } diff --git a/internal/util/template/funcs.go b/internal/util/template/funcs.go index a7a42bc83e..c9fbe90c1f 100644 --- a/internal/util/template/funcs.go +++ b/internal/util/template/funcs.go @@ -12,3 +12,14 @@ func truncateFrom(str, sep string) string { before, _, _ := strings.Cut(str, sep) return before } + +// coalesce will return the first non-empty string in the list of strings, and +// an empty string if all parameters are empty. +func coalesce(vals ...string) string { + for _, val := range vals { + if val != "" { + return val + } + } + return "" +} diff --git a/internal/util/template/generate.go b/internal/util/template/generate.go index 3eac455601..4906b3a42c 100644 --- a/internal/util/template/generate.go +++ b/internal/util/template/generate.go @@ -33,6 +33,7 @@ func New(ctx context.Context, raw string) (*Parsed, error) { raw: raw, funcMap: map[string]any{ "truncateFrom": truncateFrom, + "coalesce": coalesce, }, } diff --git a/internal/util/template/generate_test.go b/internal/util/template/generate_test.go index 52b393feba..3d65bb8de2 100644 --- a/internal/util/template/generate_test.go +++ b/internal/util/template/generate_test.go @@ -39,7 +39,7 @@ func TestErrors(t *testing.T) { require.NotNil(parsed) assert.Equal(ts, parsed.raw) assert.NotNil(parsed.tmpl) - assert.Len(parsed.funcMap, 1) + assert.Len(parsed.funcMap, 2) // Test out errors on the parsed value @@ -102,6 +102,8 @@ func TestGenerate(t *testing.T) { {{ .Account.Subject }} {{ .Account.Email }} {{ truncateFrom .Account.Email "@" }} +{{ coalesce "" .Account.LoginName .Account.Name }} +{{ coalesce "" "" .Account.Name .Account.LoginName }} `) parsed, err := New(ctx, raw) @@ -128,6 +130,8 @@ accountLoginName accountSubject account@email.com account +accountLoginName +accountName `) assert.Equal(exp, out) diff --git a/internal/util/template/types.go b/internal/util/template/types.go index ff25d4a0ed..6e934bfa4f 100644 --- a/internal/util/template/types.go +++ b/internal/util/template/types.go @@ -19,10 +19,11 @@ type Data struct { // request to be from a different auth method, in which case it may not match // what's in the Account struct. type User struct { - Id *string - Name *string - FullName *string - Email *string + Id *string + Name *string + LoginName *string + FullName *string + Email *string } // Account contains account information. Not all fields will always be diff --git a/website/content/docs/concepts/domain-model/credential-libraries.mdx b/website/content/docs/concepts/domain-model/credential-libraries.mdx index fc6807ef17..c4a3c7b52e 100644 --- a/website/content/docs/concepts/domain-model/credential-libraries.mdx +++ b/website/content/docs/concepts/domain-model/credential-libraries.mdx @@ -105,14 +105,21 @@ This value may not be populated, or it may be different from the account name us - `{{.Account.Subject}}` - The account's subject, if a subject is used by that type of account. - `{{.Account.Email}}` - The account's email, if email is used by that type of account. -Additionally, there is currently a single function that strips the rest of a string after a specified substring. -This function is useful for pulling a user or account name from an email address. -The following example turns `foo@example.com` into `foo`: +Additionally, there are a couple of useful functions: + +The `trucateFrom` function strips the rest of a string after a specified +substring. This function is useful for pulling a user or account name from an +email address. The following example turns `foo@example.com` into `foo`: `{{truncateFrom .Account.Email "@"}}` The example above uses the account email, but it could be any other parameter. +The `coalesce` function chooses the first non-empty value out of the list. This +is useful when using account names/login names since only one may be populated: + +`{{coalesce .Account.Name .Account.LoginName}}` + ## Tutorial Refer to the [SSH certificate injection with HCP Boundary](/boundary/tutorials/access-management/hcp-certificate-injection) tutorial to learn how to configure credential injection with SSH certificates using Vault.