feat(templates): templating functions (#4635)
This adds several functions which are available in most areas that use templates.pull/4637/head
parent
a916b65357
commit
55a6794370
|
@ -3,7 +3,6 @@ package main
|
|||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
|
@ -24,13 +23,11 @@ var (
|
|||
)
|
||||
|
||||
func newTMPL(name string) (tmpl *template.Template, err error) {
|
||||
return template.New(name).
|
||||
Funcs(template.FuncMap{
|
||||
"stringsContains": strings.Contains,
|
||||
"join": strings.Join,
|
||||
"joinX": templates.StringJoinXFunc,
|
||||
}).
|
||||
Parse(mustLoadTmplFS(name))
|
||||
funcs := templates.FuncMap()
|
||||
|
||||
funcs["joinX"] = templates.FuncStringJoinX
|
||||
|
||||
return template.New(name).Funcs(funcs).Parse(mustLoadTmplFS(name))
|
||||
}
|
||||
|
||||
func mustLoadTmplFS(tmpl string) string {
|
||||
|
|
|
@ -50,7 +50,7 @@ for, and the structure it must have.
|
|||
│ │
|
||||
│ └─⫸ 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.
|
||||
|
@ -59,7 +59,7 @@ The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is opti
|
|||
{{ range .Types.Details }}
|
||||
* __{{ .Name }}__ {{ .Description }}
|
||||
{{- if .Scopes }}
|
||||
(example scopes: {{ join .Scopes ", " }})
|
||||
(example scopes: {{ join ", " .Scopes }})
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ module.exports = {
|
|||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
["{{ join .Types.List "\", \"" }}"],
|
||||
["{{ join "\", \"" .Types.List }}"],
|
||||
],
|
||||
"scope-enum": [
|
||||
2,
|
||||
|
|
|
@ -37,7 +37,7 @@ i18n.use(Backend)
|
|||
default: ["{{ .Defaults.Language.Locale }}"],
|
||||
{{- range .Languages }}
|
||||
{{- 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 }}
|
||||
},
|
||||
|
|
|
@ -187,121 +187,6 @@ output at each filter stage as a base64 string when trace logging is enabled.
|
|||
|
||||
#### 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
|
||||
|
||||
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 }}'
|
||||
```
|
||||
See the [Templating Reference Guide](../../reference/guides/templating.md) for more information.
|
||||
|
|
|
@ -73,6 +73,11 @@ Some Additional examples for specific purposes can be found in the
|
|||
The original template content can be found on
|
||||
[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
|
||||
[server_name]: ../../configuration/notifications/smtp.md#tls
|
||||
[sender]: ../../configuration/notifications/smtp.md#sender
|
||||
|
|
|
@ -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.
|
|
@ -36,4 +36,7 @@ const (
|
|||
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"}
|
||||
|
|
|
@ -112,32 +112,7 @@ func NewExpandEnvFileFilter() FileFilter {
|
|||
|
||||
// NewTemplateFileFilter is a FileFilter which passes the bytes through text/template.
|
||||
func NewTemplateFileFilter() FileFilter {
|
||||
data := &TemplateFileFilterData{
|
||||
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,
|
||||
})
|
||||
t := template.New("config.template").Funcs(templates.FuncMap())
|
||||
|
||||
log := logging.Logger()
|
||||
|
||||
|
@ -148,7 +123,7 @@ func NewTemplateFileFilter() FileFilter {
|
|||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
if err = t.Execute(buf, data); err != nil {
|
||||
if err = t.Execute(buf, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -163,8 +138,3 @@ func NewTemplateFileFilter() FileFilter {
|
|||
return out, nil
|
||||
}
|
||||
}
|
||||
|
||||
// TemplateFileFilterData is the data available to the Go Template FileFilter.
|
||||
type TemplateFileFilterData struct {
|
||||
Env map[string]string
|
||||
}
|
||||
|
|
|
@ -1,41 +1,193 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"crypto/sha1" //nolint:gosec
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StringMapLookupDefaultEmptyFunc is function which takes a map[string]string and returns a template function which
|
||||
// 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
|
||||
// string.
|
||||
func StringMapLookupDefaultEmptyFunc(m map[string]string) func(key string) (value string) {
|
||||
return func(key string) (value string) {
|
||||
var ok bool
|
||||
// FuncMap returns the template FuncMap commonly used in several templates.
|
||||
func FuncMap() map[string]any {
|
||||
return map[string]any{
|
||||
"iterate": FuncIterate,
|
||||
"env": FuncGetEnv,
|
||||
"expandenv": FuncExpandEnv,
|
||||
"split": FuncStringSplit,
|
||||
"splitList": FuncStringSplitList,
|
||||
"join": FuncElemsJoin,
|
||||
"contains": FuncStringContains,
|
||||
"hasPrefix": FuncStringHasPrefix,
|
||||
"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,
|
||||
}
|
||||
}
|
||||
|
||||
if value, ok = m[key]; !ok {
|
||||
// FuncExpandEnv is a special version of os.ExpandEnv that excludes secret keys.
|
||||
func FuncExpandEnv(s string) string {
|
||||
return os.Expand(s, FuncGetEnv)
|
||||
}
|
||||
|
||||
// FuncGetEnv is a special version of os.GetEnv that excludes secret keys.
|
||||
func FuncGetEnv(key string) string {
|
||||
if isSecretEnvKey(key) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return value
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// StringMapLookupFunc is function which takes a map[string]string and returns a template function which
|
||||
// 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 StringMapLookupFunc(m map[string]string) func(key string) (value string, err error) {
|
||||
return func(key string) (value string, err error) {
|
||||
var ok bool
|
||||
// FuncStringReplace is a helper function that provides similar functionality to the helm replace func.
|
||||
func FuncStringReplace(old, new, s string) string {
|
||||
return strings.ReplaceAll(s, old, new)
|
||||
}
|
||||
|
||||
if value, ok = m[key]; !ok {
|
||||
return value, fmt.Errorf("failed to lookup key '%s' from map", key)
|
||||
// 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 value, nil
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// IterateFunc is a template function which takes a single uint returning a slice of units from 0 up to that number.
|
||||
func IterateFunc(count *uint) (out []uint) {
|
||||
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
|
||||
|
||||
for i = 0; i < (*count); i++ {
|
||||
|
@ -45,14 +197,26 @@ func IterateFunc(count *uint) (out []uint) {
|
|||
return
|
||||
}
|
||||
|
||||
// StringsSplitFunc is a template function which takes sep and value, splitting the value by the sep into a slice.
|
||||
func StringsSplitFunc(value, sep string) []string {
|
||||
return strings.Split(value, sep)
|
||||
// FuncStringSplit is a template function which takes sep and value, splitting the value by the sep into a slice.
|
||||
func FuncStringSplit(sep, value string) map[string]string {
|
||||
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.
|
||||
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{}
|
||||
|
||||
c := 0
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -6,9 +6,39 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
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) {
|
||||
info, err := os.Stat(path)
|
||||
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) {
|
||||
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 {
|
||||
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) {
|
||||
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 {
|
||||
return nil, fmt.Errorf("failed to parse embedded template '%s': %w", tPath, err)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue