feat(templates): templating functions (#4635)

This adds several functions which are available in most areas that use templates.
pull/4637/head
James Elliott 2022-12-23 21:58:54 +11:00 committed by GitHub
parent a916b65357
commit 55a6794370
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 677 additions and 191 deletions

View File

@ -3,7 +3,6 @@ package main
import ( import (
"embed" "embed"
"fmt" "fmt"
"strings"
"text/template" "text/template"
"github.com/authelia/authelia/v4/internal/templates" "github.com/authelia/authelia/v4/internal/templates"
@ -24,13 +23,11 @@ var (
) )
func newTMPL(name string) (tmpl *template.Template, err error) { func newTMPL(name string) (tmpl *template.Template, err error) {
return template.New(name). funcs := templates.FuncMap()
Funcs(template.FuncMap{
"stringsContains": strings.Contains, funcs["joinX"] = templates.FuncStringJoinX
"join": strings.Join,
"joinX": templates.StringJoinXFunc, return template.New(name).Funcs(funcs).Parse(mustLoadTmplFS(name))
}).
Parse(mustLoadTmplFS(name))
} }
func mustLoadTmplFS(tmpl string) string { func mustLoadTmplFS(tmpl string) string {

View File

@ -50,7 +50,7 @@ for, and the structure it must have.
│ │ │ │
│ └─⫸ Commit Scope: {{ joinX .Scopes.All "|" 70 "\n │ " }} │ └─⫸ Commit Scope: {{ joinX .Scopes.All "|" 70 "\n │ " }}
└─⫸ Commit Type: {{ join .Types.List "|" }} └─⫸ Commit Type: {{ join "|" .Types.List }}
``` ```
The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional. The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional.
@ -59,7 +59,7 @@ The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is opti
{{ range .Types.Details }} {{ range .Types.Details }}
* __{{ .Name }}__ {{ .Description }} * __{{ .Name }}__ {{ .Description }}
{{- if .Scopes }} {{- if .Scopes }}
(example scopes: {{ join .Scopes ", " }}) (example scopes: {{ join ", " .Scopes }})
{{- end }} {{- end }}
{{- end }} {{- end }}

View File

@ -8,7 +8,7 @@ module.exports = {
"type-enum": [ "type-enum": [
2, 2,
"always", "always",
["{{ join .Types.List "\", \"" }}"], ["{{ join "\", \"" .Types.List }}"],
], ],
"scope-enum": [ "scope-enum": [
2, 2,

View File

@ -37,7 +37,7 @@ i18n.use(Backend)
default: ["{{ .Defaults.Language.Locale }}"], default: ["{{ .Defaults.Language.Locale }}"],
{{- range .Languages }} {{- range .Languages }}
{{- if and (not (eq .Locale "en")) (not (eq (len .Fallbacks) 0)) }} {{- if and (not (eq .Locale "en")) (not (eq (len .Fallbacks) 0)) }}
{{ if stringsContains .Locale "-" }}"{{ .Locale }}"{{ else }}{{ .Locale }}{{ end }}: [{{ range $i, $value := .Fallbacks }}{{ if eq $i 0 }}"{{ $value }}"{{ else }}, "{{ $value }}"{{ end }}{{ end }}], {{ if contains "-" .Locale }}"{{ .Locale }}"{{ else }}{{ .Locale }}{{ end }}: [{{ range $i, $value := .Fallbacks }}{{ if eq $i 0 }}"{{ $value }}"{{ else }}, "{{ $value }}"{{ end }}{{ end }}],
{{- end }} {{- end }}
{{- end }} {{- end }}
}, },

View File

@ -187,121 +187,6 @@ output at each filter stage as a base64 string when trace logging is enabled.
#### Functions #### Functions
In addition to the standard builtin functions we support several other functions. In addition to the standard builtin functions we support several other functions which should operate similar.
##### iterate See the [Templating Reference Guide](../../reference/guides/templating.md) for more information.
The `iterate` function generates a list of numbers from 0 to the input provided. Useful for ranging over a list of
numbers.
Example:
```yaml
numbers:
{{- range $i := iterate 5 }}
- {{ $i }}
{{- end }}
```
##### env
The `env` function returns the value of an environment variable or a blank string.
Example:
```yaml
default_redirection_url: 'https://{{ env "DOMAIN" }}'
```
##### split
The `split` function splits a string by the separator.
Example:
```yaml
access_control:
rules:
- domain: 'app.{{ env "DOMAIN" }}'
policy: bypass
methods:
{{ range _, $method := split "GET,POST" "," }}
- {{ $method }}
{{ end }}
```
##### join
The `join` function is similar to [split](#split) but does the complete oppiste, joining an array of strings with a
separator.
Example:
```yaml
access_control:
rules:
- domain: ['app.{{ join (split (env "DOMAINS") ",") "', 'app." }}']
policy: bypass
```
##### contains
The `contains` function is a test function which checks if one string contains another string.
Example:
```yaml
{{ if contains (env "DOMAIN") "https://" }}
default_redirection_url: '{{ env "DOMAIN" }}'
{{ else }}
default_redirection_url: 'https://{{ env "DOMAIN" }}'
{{ end }}
```
##### hasPrefix
The `hasPrefix` function is a test function which checks if one string is prefixed with another string.
Example:
```yaml
{{ if hasPrefix (env "DOMAIN") "https://" }}
default_redirection_url: '{{ env "DOMAIN" }}'
{{ else }}
default_redirection_url: 'https://{{ env "DOMAIN" }}'
{{ end }}
```
##### hasSuffix
The `hasSuffix` function is a test function which checks if one string is suffixed with another string.
Example:
```yaml
{{ if hasSuffix (env "DOMAIN") "/" }}
default_redirection_url: 'https://{{ env "DOMAIN" }}'
{{ else }}
default_redirection_url: 'https://{{ env "DOMAIN" }}/'
{{ end }}
```
##### lower
The `lower` function is a conversion function which converts a string to all lowercase.
Example:
```yaml
default_redirection_url: 'https://{{ env "DOMAIN" | lower }}'
```
##### upper
The `upper` function is a conversion function which converts a string to all uppercase.
Example:
```yaml
default_redirection_url: 'https://{{ env "DOMAIN" | upper }}'
```

View File

@ -73,6 +73,11 @@ Some Additional examples for specific purposes can be found in the
The original template content can be found on The original template content can be found on
[GitHub](https://github.com/authelia/authelia/tree/master/internal/templates/src/notification). [GitHub](https://github.com/authelia/authelia/tree/master/internal/templates/src/notification).
## Functions
Several functions are implemented with the email templates. See the
[Templating Reference Guide](../../reference/guides/templating.md) for more information.
[host]: ../../configuration/notifications/smtp.md#host [host]: ../../configuration/notifications/smtp.md#host
[server_name]: ../../configuration/notifications/smtp.md#tls [server_name]: ../../configuration/notifications/smtp.md#tls
[sender]: ../../configuration/notifications/smtp.md#sender [sender]: ../../configuration/notifications/smtp.md#sender

View File

@ -0,0 +1,66 @@
---
title: "Templating"
description: "A reference guide on the templates system"
lead: "This section contains reference documentation for Authelia's templating capabilities."
date: 2022-12-23T18:31:05+11:00
draft: false
images: []
menu:
reference:
parent: "guides"
weight: 220
toc: true
---
Authelia has several methods where users can interact with templates.
## Functions
Functions can be used to perform specific actions when executing templates. The following is a simple guide on which
functions exist.
### Standard Functions
Go has a set of standard functions which can be used. See the [Go Documentation](https://pkg.go.dev/text/template#hdr-Functions)
for more information.
### Helm-like Functions
The following functions which mimic the behaviour of helm exist in most templating areas:
- env
- expandenv
- split
- splitList
- join
- contains
- hasPrefix
- hasSuffix
- lower
- upper
- title
- trim
- trimAll
- trimSuffix
- trimPrefix
- replace
- quote
- sha1sum
- sha256sum
- sha512sum
- squote
- now
See the [Helm Documentation](https://helm.sh/docs/chart_template_guide/function_list/) for more information. Please
note that only the functions listed above are supported.
__*Special Note:* The `env` and `expandenv` function automatically excludes environment variables that start with
`AUTHELIA_` or `X_AUTHELIA_` and end with one of `KEY`, `SECRET`, `PASSWORD`, `TOKEN`, or `CERTIFICATE_CHAIN`.__
### Special Functions
The following is a list of special functions and their syntax.
#### iterate
Input is a single uint. Returns a slice of uints from 0 to the provided uint.

View File

@ -36,4 +36,7 @@ const (
errFmtDecodeHookCouldNotParseEmptyValue = "could not decode an empty value to a %s%s: %w" errFmtDecodeHookCouldNotParseEmptyValue = "could not decode an empty value to a %s%s: %w"
) )
// IMPORTANT: There is an uppercase copy of this in github.com/authelia/authelia/internal/templates named
// envSecretSuffixes.
// Make sure you update these at the same time.
var secretSuffixes = []string{"key", "secret", "password", "token", "certificate_chain"} var secretSuffixes = []string{"key", "secret", "password", "token", "certificate_chain"}

View File

@ -112,32 +112,7 @@ func NewExpandEnvFileFilter() FileFilter {
// NewTemplateFileFilter is a FileFilter which passes the bytes through text/template. // NewTemplateFileFilter is a FileFilter which passes the bytes through text/template.
func NewTemplateFileFilter() FileFilter { func NewTemplateFileFilter() FileFilter {
data := &TemplateFileFilterData{ t := template.New("config.template").Funcs(templates.FuncMap())
Env: map[string]string{},
}
for _, e := range os.Environ() {
kv := strings.SplitN(e, "=", 2)
if len(kv) != 2 {
continue
}
data.Env[kv[0]] = kv[1]
}
t := template.New("config.template").
Funcs(template.FuncMap{
"env": templates.StringMapLookupDefaultEmptyFunc(data.Env),
"split": templates.StringsSplitFunc,
"iterate": templates.IterateFunc,
"join": strings.Join,
"contains": strings.Contains,
"hasPrefix": strings.HasPrefix,
"hasSuffix": strings.HasSuffix,
"lower": strings.ToLower,
"upper": strings.ToUpper,
})
log := logging.Logger() log := logging.Logger()
@ -148,7 +123,7 @@ func NewTemplateFileFilter() FileFilter {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
if err = t.Execute(buf, data); err != nil { if err = t.Execute(buf, nil); err != nil {
return nil, err return nil, err
} }
@ -163,8 +138,3 @@ func NewTemplateFileFilter() FileFilter {
return out, nil return out, nil
} }
} }
// TemplateFileFilterData is the data available to the Go Template FileFilter.
type TemplateFileFilterData struct {
Env map[string]string
}

View File

@ -1,41 +1,193 @@
package templates package templates
import ( import (
"crypto/sha1" //nolint:gosec
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"fmt" "fmt"
"hash"
"os"
"reflect"
"strconv"
"strings" "strings"
"time"
) )
// StringMapLookupDefaultEmptyFunc is function which takes a map[string]string and returns a template function which // FuncMap returns the template FuncMap commonly used in several templates.
// takes a string which is used as a key lookup for the map[string]string. If the value isn't found it returns an empty func FuncMap() map[string]any {
// string. return map[string]any{
func StringMapLookupDefaultEmptyFunc(m map[string]string) func(key string) (value string) { "iterate": FuncIterate,
return func(key string) (value string) { "env": FuncGetEnv,
var ok bool "expandenv": FuncExpandEnv,
"split": FuncStringSplit,
if value, ok = m[key]; !ok { "splitList": FuncStringSplitList,
return "" "join": FuncElemsJoin,
} "contains": FuncStringContains,
"hasPrefix": FuncStringHasPrefix,
return value "hasSuffix": FuncStringHasSuffix,
"lower": strings.ToLower,
"upper": strings.ToUpper,
"title": strings.ToTitle,
"trim": strings.TrimSpace,
"trimAll": FuncStringTrimAll,
"trimSuffix": FuncStringTrimSuffix,
"trimPrefix": FuncStringTrimPrefix,
"replace": FuncStringReplace,
"quote": FuncStringQuote,
"sha1sum": FuncHashSum(sha1.New),
"sha256sum": FuncHashSum(sha256.New),
"sha512sum": FuncHashSum(sha512.New),
"squote": FuncStringSQuote,
"now": time.Now,
} }
} }
// StringMapLookupFunc is function which takes a map[string]string and returns a template function which // FuncExpandEnv is a special version of os.ExpandEnv that excludes secret keys.
// takes a string which is used as a key lookup for the map[string]string. If the value isn't found it returns an error. func FuncExpandEnv(s string) string {
func StringMapLookupFunc(m map[string]string) func(key string) (value string, err error) { return os.Expand(s, FuncGetEnv)
return func(key string) (value string, err error) { }
var ok bool
if value, ok = m[key]; !ok { // FuncGetEnv is a special version of os.GetEnv that excludes secret keys.
return value, fmt.Errorf("failed to lookup key '%s' from map", key) func FuncGetEnv(key string) string {
} if isSecretEnvKey(key) {
return ""
}
return value, nil return os.Getenv(key)
}
// FuncHashSum is a helper function that provides similar functionality to helm sum funcs.
func FuncHashSum(new func() hash.Hash) func(data string) string {
hasher := new()
return func(data string) string {
sum := hasher.Sum([]byte(data))
return hex.EncodeToString(sum)
} }
} }
// IterateFunc is a template function which takes a single uint returning a slice of units from 0 up to that number. // FuncStringReplace is a helper function that provides similar functionality to the helm replace func.
func IterateFunc(count *uint) (out []uint) { func FuncStringReplace(old, new, s string) string {
return strings.ReplaceAll(s, old, new)
}
// FuncStringContains is a helper function that provides similar functionality to the helm contains func.
func FuncStringContains(substr string, s string) bool {
return strings.Contains(s, substr)
}
// FuncStringHasPrefix is a helper function that provides similar functionality to the helm hasPrefix func.
func FuncStringHasPrefix(prefix string, s string) bool {
return strings.HasPrefix(s, prefix)
}
// FuncStringHasSuffix is a helper function that provides similar functionality to the helm hasSuffix func.
func FuncStringHasSuffix(suffix string, s string) bool {
return strings.HasSuffix(s, suffix)
}
// FuncStringTrimAll is a helper function that provides similar functionality to the helm trimAll func.
func FuncStringTrimAll(cutset, s string) string {
return strings.Trim(s, cutset)
}
// FuncStringTrimSuffix is a helper function that provides similar functionality to the helm trimSuffix func.
func FuncStringTrimSuffix(suffix, s string) string {
return strings.TrimSuffix(s, suffix)
}
// FuncStringTrimPrefix is a helper function that provides similar functionality to the helm trimPrefix func.
func FuncStringTrimPrefix(prefix, s string) string {
return strings.TrimPrefix(s, prefix)
}
// FuncElemsJoin is a helper function that provides similar functionality to the helm join func.
func FuncElemsJoin(sep string, elems any) string {
return strings.Join(strslice(elems), sep)
}
// FuncStringSQuote is a helper function that provides similar functionality to the helm squote func.
func FuncStringSQuote(in ...any) string {
out := make([]string, 0, len(in))
for _, s := range in {
if s != nil {
out = append(out, fmt.Sprintf("%q", strval(s)))
}
}
return strings.Join(out, " ")
}
// FuncStringQuote is a helper function that provides similar functionality to the helm quote func.
func FuncStringQuote(in ...any) string {
out := make([]string, 0, len(in))
for _, s := range in {
if s != nil {
out = append(out, fmt.Sprintf("%q", strval(s)))
}
}
return strings.Join(out, " ")
}
func strval(v interface{}) string {
switch v := v.(type) {
case string:
return v
case []byte:
return string(v)
case fmt.Stringer:
return v.String()
default:
return fmt.Sprintf("%v", v)
}
}
func strslice(v any) []string {
switch v := v.(type) {
case []string:
return v
case []interface{}:
b := make([]string, 0, len(v))
for _, s := range v {
if s != nil {
b = append(b, strval(s))
}
}
return b
default:
val := reflect.ValueOf(v)
switch val.Kind() {
case reflect.Array, reflect.Slice:
l := val.Len()
b := make([]string, 0, l)
for i := 0; i < l; i++ {
value := val.Index(i).Interface()
if value != nil {
b = append(b, strval(value))
}
}
return b
default:
if v == nil {
return []string{}
}
return []string{strval(v)}
}
}
}
// FuncIterate is a template function which takes a single uint returning a slice of units from 0 up to that number.
func FuncIterate(count *uint) (out []uint) {
var i uint var i uint
for i = 0; i < (*count); i++ { for i = 0; i < (*count); i++ {
@ -45,14 +197,26 @@ func IterateFunc(count *uint) (out []uint) {
return return
} }
// StringsSplitFunc is a template function which takes sep and value, splitting the value by the sep into a slice. // FuncStringSplit is a template function which takes sep and value, splitting the value by the sep into a slice.
func StringsSplitFunc(value, sep string) []string { func FuncStringSplit(sep, value string) map[string]string {
return strings.Split(value, sep) parts := strings.Split(value, sep)
res := make(map[string]string, len(parts))
for i, v := range parts {
res["_"+strconv.Itoa(i)] = v
}
return res
} }
// StringJoinXFunc takes a list of string elements, joins them by the sep string, before every int n characters are // FuncStringSplitList is a special split func that reverses the inputs to match helm templates.
func FuncStringSplitList(sep, s string) []string {
return strings.Split(s, sep)
}
// FuncStringJoinX takes a list of string elements, joins them by the sep string, before every int n characters are
// written it writes string p. This is useful for line breaks mostly. // written it writes string p. This is useful for line breaks mostly.
func StringJoinXFunc(elems []string, sep string, n int, p string) string { func FuncStringJoinX(elems []string, sep string, n int, p string) string {
buf := strings.Builder{} buf := strings.Builder{}
c := 0 c := 0

View File

@ -0,0 +1,336 @@
package templates
import (
"crypto/sha1" //nolint:gosec
"crypto/sha256"
"crypto/sha512"
"hash"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFuncGetEnv(t *testing.T) {
testCases := []struct {
name string
have map[string]string
expected map[string]string
}{
{"ShouldGetEnv",
map[string]string{
"AN_ENV": "a",
"ANOTHER_ENV": "b",
},
map[string]string{
"AN_ENV": "a",
"ANOTHER_ENV": "b",
},
},
{"ShouldNotGetSecretEnv",
map[string]string{
"AUTHELIA_ENV_SECRET": "a",
"ANOTHER_ENV": "b",
},
map[string]string{
"AUTHELIA_ENV_SECRET": "",
"ANOTHER_ENV": "b",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for key, value := range tc.have {
assert.NoError(t, os.Setenv(key, value))
}
for key, expected := range tc.expected {
assert.Equal(t, expected, FuncGetEnv(key))
}
for key := range tc.have {
assert.NoError(t, os.Unsetenv(key))
}
})
}
}
func TestFuncExpandEnv(t *testing.T) {
testCases := []struct {
name string
env map[string]string
have string
expected string
}{
{"ShouldExpandEnv",
map[string]string{
"AN_ENV": "a",
"ANOTHER_ENV": "b",
},
"This is ${AN_ENV} and ${ANOTHER_ENV}",
"This is a and b",
},
{"ShouldNotExpandSecretEnv",
map[string]string{
"AUTHELIA_ENV_SECRET": "a",
"X_AUTHELIA_ENV_SECRET": "a",
"ANOTHER_ENV": "b",
},
"This is ${AUTHELIA_ENV_SECRET} and ${ANOTHER_ENV} without ${X_AUTHELIA_ENV_SECRET}",
"This is and b without ",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for key, value := range tc.env {
assert.NoError(t, os.Setenv(key, value))
}
assert.Equal(t, tc.expected, FuncExpandEnv(tc.have))
for key := range tc.env {
assert.NoError(t, os.Unsetenv(key))
}
})
}
}
func TestFuncHashSum(t *testing.T) {
testCases := []struct {
name string
new func() hash.Hash
have []string
expected []string
}{
{"ShouldHashSHA1", sha1.New, []string{"abc", "123", "authelia"}, []string{"616263da39a3ee5e6b4b0d3255bfef95601890afd80709", "313233da39a3ee5e6b4b0d3255bfef95601890afd80709", "61757468656c6961da39a3ee5e6b4b0d3255bfef95601890afd80709"}},
{"ShouldHashSHA256", sha256.New, []string{"abc", "123", "authelia"}, []string{"616263e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "313233e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "61757468656c6961e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},
{"ShouldHashSHA512", sha512.New, []string{"abc", "123", "authelia"}, []string{"616263cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", "313233cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", "61757468656c6961cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, len(tc.have), len(tc.expected))
h := FuncHashSum(tc.new)
for i := 0; i < len(tc.have); i++ {
assert.Equal(t, tc.expected[i], h(tc.have[i]))
}
})
}
}
func TestFuncStringReplace(t *testing.T) {
testCases := []struct {
name string
have string
old, new string
expected string
}{
{"ShouldReplaceSingle", "ABC123", "123", "456", "ABC456"},
{"ShouldReplaceMultiple", "123ABC123123", "123", "456", "456ABC456456"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncStringReplace(tc.old, tc.new, tc.have))
})
}
}
func TestFuncStringContains(t *testing.T) {
testCases := []struct {
name string
have string
substr string
expected bool
}{
{"ShouldMatchNormal", "abc123", "c12", true},
{"ShouldNotMatchWrongCase", "abc123", "C12", false},
{"ShouldNotMatchNotContains", "abc123", "xyz", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncStringContains(tc.substr, tc.have))
})
}
}
func TestFuncStringHasPrefix(t *testing.T) {
testCases := []struct {
name string
have string
substr string
expected bool
}{
{"ShouldMatchNormal", "abc123", "abc", true},
{"ShouldNotMatchWrongCase", "abc123", "ABC", false},
{"ShouldNotMatchNotPrefix", "abc123", "123", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncStringHasPrefix(tc.substr, tc.have))
})
}
}
func TestFuncStringHasSuffix(t *testing.T) {
testCases := []struct {
name string
have string
substr string
expected bool
}{
{"ShouldMatchNormal", "abc123xyz", "xyz", true},
{"ShouldNotMatchWrongCase", "abc123xyz", "XYZ", false},
{"ShouldNotMatchNotSuffix", "abc123xyz", "123", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncStringHasSuffix(tc.substr, tc.have))
})
}
}
func TestFuncStringTrimAll(t *testing.T) {
testCases := []struct {
name string
have string
cutset string
expected string
}{
{"ShouldTrimSuffix", "abc123xyz", "xyz", "abc123"},
{"ShouldTrimPrefix", "xyzabc123", "xyz", "abc123"},
{"ShouldNotTrimMiddle", "abcxyz123", "xyz", "abcxyz123"},
{"ShouldNotTrimWrongCase", "xyzabcxyz123xyz", "XYZ", "xyzabcxyz123xyz"},
{"ShouldNotTrimWrongChars", "abc123xyz", "456", "abc123xyz"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncStringTrimAll(tc.cutset, tc.have))
})
}
}
func TestFuncStringTrimPrefix(t *testing.T) {
testCases := []struct {
name string
have string
cutset string
expected string
}{
{"ShouldNotTrimSuffix", "abc123xyz", "xyz", "abc123xyz"},
{"ShouldTrimPrefix", "xyzabc123", "xyz", "abc123"},
{"ShouldNotTrimMiddle", "abcxyz123", "xyz", "abcxyz123"},
{"ShouldNotTrimWrongCase", "xyzabcxyz123xyz", "XYZ", "xyzabcxyz123xyz"},
{"ShouldNotTrimWrongChars", "abc123xyz", "456", "abc123xyz"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncStringTrimPrefix(tc.cutset, tc.have))
})
}
}
func TestFuncStringTrimSuffix(t *testing.T) {
testCases := []struct {
name string
have string
cutset string
expected string
}{
{"ShouldTrimSuffix", "abc123xyz", "xyz", "abc123"},
{"ShouldNotTrimPrefix", "xyzabc123", "xyz", "xyzabc123"},
{"ShouldNotTrimMiddle", "abcxyz123", "xyz", "abcxyz123"},
{"ShouldNotTrimWrongCase", "xyzabcxyz123xyz", "XYZ", "xyzabcxyz123xyz"},
{"ShouldNotTrimWrongChars", "abc123xyz", "456", "abc123xyz"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncStringTrimSuffix(tc.cutset, tc.have))
})
}
}
func TestFuncElemsJoin(t *testing.T) {
testCases := []struct {
name string
have any
sep string
expected string
}{
{"ShouldNotJoinNonElements", "abc123xyz", "xyz", "abc123xyz"},
{"ShouldJoinStrings", []string{"abc", "123"}, "xyz", "abcxyz123"},
{"ShouldJoinInts", []int{1, 2, 3}, ",", "1,2,3"},
{"ShouldJoinBooleans", []bool{true, false, true}, ".", "true.false.true"},
{"ShouldJoinBytes", [][]byte{[]byte("abc"), []byte("123"), []byte("a")}, "$", "abc$123$a"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncElemsJoin(tc.sep, tc.have))
})
}
}
func TestFuncIterate(t *testing.T) {
testCases := []struct {
name string
have uint
expected []uint
}{
{"ShouldGiveZeroResults", 0, nil},
{"ShouldGive10Results", 10, []uint{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncIterate(&tc.have))
})
}
}
func TestFuncStringsSplit(t *testing.T) {
testCases := []struct {
name string
have string
sep string
expected map[string]string
}{
{"ShouldSplit", "abc,123,456", ",", map[string]string{"_0": "abc", "_1": "123", "_2": "456"}},
{"ShouldNotSplit", "abc,123,456", "$", map[string]string{"_0": "abc,123,456"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncStringSplit(tc.sep, tc.have))
})
}
}
func TestFuncStringSplitList(t *testing.T) {
testCases := []struct {
name string
have string
sep string
expected []string
}{
{"ShouldSplit", "abc,123,456", ",", []string{"abc", "123", "456"}},
{"ShouldNotSplit", "abc,123,456", "$", []string{"abc,123,456"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncStringSplitList(tc.sep, tc.have))
})
}
}

View File

@ -6,9 +6,39 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings"
tt "text/template" tt "text/template"
) )
const (
envPrefix = "AUTHELIA_"
envXPrefix = "X_AUTHELIA_"
)
// IMPORTANT: This is a copy of github.com/authelia/authelia/internal/configuration's secretSuffixes except all uppercase.
// Make sure you update these at the same time.
var envSecretSuffixes = []string{
"KEY", "SECRET", "PASSWORD", "TOKEN", "CERTIFICATE_CHAIN",
}
func isSecretEnvKey(key string) (isSecretEnvKey bool) {
key = strings.ToUpper(key)
if !strings.HasPrefix(key, envPrefix) && !strings.HasPrefix(key, envXPrefix) {
return false
}
for _, s := range envSecretSuffixes {
suffix := strings.ToUpper(s)
if strings.HasSuffix(key, suffix) {
return true
}
}
return false
}
func templateExists(path string) (exists bool) { func templateExists(path string) (exists bool) {
info, err := os.Stat(path) info, err := os.Stat(path)
if err != nil { if err != nil {
@ -45,7 +75,7 @@ func readTemplate(name, ext, category, overridePath string) (tPath string, embed
} }
func parseTextTemplate(name, tPath string, embed bool, data []byte) (t *tt.Template, err error) { func parseTextTemplate(name, tPath string, embed bool, data []byte) (t *tt.Template, err error) {
if t, err = tt.New(name + extText).Parse(string(data)); err != nil { if t, err = tt.New(name + extText).Funcs(FuncMap()).Parse(string(data)); err != nil {
if embed { if embed {
return nil, fmt.Errorf("failed to parse embedded template '%s': %w", tPath, err) return nil, fmt.Errorf("failed to parse embedded template '%s': %w", tPath, err)
} }
@ -57,7 +87,7 @@ func parseTextTemplate(name, tPath string, embed bool, data []byte) (t *tt.Templ
} }
func parseHTMLTemplate(name, tPath string, embed bool, data []byte) (t *th.Template, err error) { func parseHTMLTemplate(name, tPath string, embed bool, data []byte) (t *th.Template, err error) {
if t, err = th.New(name + extHTML).Parse(string(data)); err != nil { if t, err = th.New(name + extHTML).Funcs(FuncMap()).Parse(string(data)); err != nil {
if embed { if embed {
return nil, fmt.Errorf("failed to parse embedded template '%s': %w", tPath, err) return nil, fmt.Errorf("failed to parse embedded template '%s': %w", tPath, err)
} }

View File

@ -0,0 +1,30 @@
package templates
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsSecretEnvKey(t *testing.T) {
testCases := []struct {
name string
have []string
expected bool
}{
{"ShouldReturnFalseForKeysWithoutPrefix", []string{"A_KEY", "A_SECRET", "A_PASSWORD", "NOT_AUTHELIA_A_PASSWORD"}, false},
{"ShouldReturnFalseForKeysWithoutSuffix", []string{"AUTHELIA_EXAMPLE", "X_AUTHELIA_EXAMPLE", "X_AUTHELIA_PASSWORD_NOT"}, false},
{"ShouldReturnTrueForSecretKeys", []string{"AUTHELIA_JWT_SECRET", "AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET", "AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN", "X_AUTHELIA_JWT_SECRET", "X_AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET", "X_AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN"}, true},
{"ShouldReturnTrueForSecretKeysEvenWithMixedCase", []string{"aUTHELIA_JWT_SECRET", "aUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET", "aUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN", "X_aUTHELIA_JWT_SECREt", "X_aUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET", "x_AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN"}, true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for _, env := range tc.have {
t.Run(env, func(t *testing.T) {
assert.Equal(t, tc.expected, isSecretEnvKey(env))
})
}
})
}
}