1package template
2
3import (
4	"fmt"
5	"strings"
6	"text/template"
7
8	"github.com/hashicorp/go-multierror"
9	"github.com/hashicorp/vault/sdk/helper/base62"
10)
11
12type Opt func(*StringTemplate) error
13
14func Template(rawTemplate string) Opt {
15	return func(up *StringTemplate) error {
16		up.rawTemplate = rawTemplate
17		return nil
18	}
19}
20
21// Function allows the user to specify functions for use in the template. If the name provided is a function that
22// already exists in the function map, this will override the previously specified function.
23func Function(name string, f interface{}) Opt {
24	return func(up *StringTemplate) error {
25		if name == "" {
26			return fmt.Errorf("missing function name")
27		}
28		if f == nil {
29			return fmt.Errorf("missing function")
30		}
31		up.funcMap[name] = f
32		return nil
33	}
34}
35
36// StringTemplate creates strings based on the provided template.
37// This uses the go templating language, so anything that adheres to that language will function in this struct.
38// There are several custom functions available for use in the template:
39// - random
40//   - Randomly generated characters. This uses the charset specified in RandomCharset. Must include a length.
41//     Example: {{ rand 20 }}
42// - truncate
43//   - Truncates the previous value to the specified length. Must include a maximum length.
44//     Example: {{ .DisplayName | truncate 10 }}
45// - truncate_sha256
46//   - Truncates the previous value to the specified length. If the original length is greater than the length
47//     specified, the remaining characters will be sha256 hashed and appended to the end. The hash will be only the first 8 characters The maximum length will
48//     be no longer than the length specified.
49//     Example: {{ .DisplayName | truncate_sha256 30 }}
50// - uppercase
51//   - Uppercases the previous value.
52//     Example: {{ .RoleName | uppercase }}
53// - lowercase
54//   - Lowercases the previous value.
55//     Example: {{ .DisplayName | lowercase }}
56// - replace
57//   - Performs a string find & replace
58//     Example: {{ .DisplayName | replace - _ }}
59// - sha256
60//   - SHA256 hashes the previous value.
61//     Example: {{ .DisplayName | sha256 }}
62// - base64
63//   - base64 encodes the previous value.
64//     Example: {{ .DisplayName | base64 }}
65// - unix_time
66//   - Provides the current unix time in seconds.
67//     Example: {{ unix_time }}
68// - unix_time_millis
69//   - Provides the current unix time in milliseconds.
70//     Example: {{ unix_time_millis }}
71// - timestamp
72//   - Provides the current time. Must include a standard Go format string
73// - uuid
74//   - Generates a UUID
75//     Example: {{ uuid }}
76type StringTemplate struct {
77	rawTemplate string
78	tmpl        *template.Template
79	funcMap     template.FuncMap
80}
81
82// NewTemplate creates a StringTemplate. No arguments are required
83// as this has reasonable defaults for all values.
84// The default template is specified in the DefaultTemplate constant.
85func NewTemplate(opts ...Opt) (up StringTemplate, err error) {
86	up = StringTemplate{
87		funcMap: map[string]interface{}{
88			"random":          base62.Random,
89			"truncate":        truncate,
90			"truncate_sha256": truncateSHA256,
91			"uppercase":       uppercase,
92			"lowercase":       lowercase,
93			"replace":         replace,
94			"sha256":          hashSHA256,
95			"base64":          encodeBase64,
96
97			"unix_time":        unixTime,
98			"unix_time_millis": unixTimeMillis,
99			"timestamp":        timestamp,
100			"uuid":             uuid,
101		},
102	}
103
104	merr := &multierror.Error{}
105	for _, opt := range opts {
106		merr = multierror.Append(merr, opt(&up))
107	}
108
109	err = merr.ErrorOrNil()
110	if err != nil {
111		return up, err
112	}
113
114	if up.rawTemplate == "" {
115		return StringTemplate{}, fmt.Errorf("missing template")
116	}
117
118	tmpl, err := template.New("template").
119		Funcs(up.funcMap).
120		Parse(up.rawTemplate)
121	if err != nil {
122		return StringTemplate{}, fmt.Errorf("unable to parse template: %w", err)
123	}
124	up.tmpl = tmpl
125
126	return up, nil
127}
128
129// Generate based on the provided template
130func (up StringTemplate) Generate(data interface{}) (string, error) {
131	if up.tmpl == nil || up.rawTemplate == "" {
132		return "", fmt.Errorf("failed to generate: template not initialized")
133	}
134	str := &strings.Builder{}
135	err := up.tmpl.Execute(str, data)
136	if err != nil {
137		return "", fmt.Errorf("unable to apply template: %w", err)
138	}
139
140	return str.String(), nil
141}
142