mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 22:15:51 +00:00
f81348a16b
... all prefixed with TS_DEBUGSYSPOLICY_*. Updates #13193 Updates #12687 Updates #13855 Change-Id: Ia8024946f53e2b3afda4456a7bb85bbcf6d12bfc Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
160 lines
4.2 KiB
Go
160 lines
4.2 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package source
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"tailscale.com/util/syspolicy/setting"
|
|
)
|
|
|
|
var lookupEnv = os.LookupEnv // test hook
|
|
|
|
var _ Store = (*EnvPolicyStore)(nil)
|
|
|
|
// EnvPolicyStore is a [Store] that reads policy settings from environment variables.
|
|
type EnvPolicyStore struct{}
|
|
|
|
// ReadString implements [Store].
|
|
func (s *EnvPolicyStore) ReadString(key setting.Key) (string, error) {
|
|
_, str, err := s.lookupSettingVariable(key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return str, nil
|
|
}
|
|
|
|
// ReadUInt64 implements [Store].
|
|
func (s *EnvPolicyStore) ReadUInt64(key setting.Key) (uint64, error) {
|
|
name, str, err := s.lookupSettingVariable(key)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if str == "" {
|
|
return 0, setting.ErrNotConfigured
|
|
}
|
|
value, err := strconv.ParseUint(str, 0, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s: %w: %q is not a valid uint64", name, setting.ErrTypeMismatch, str)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
// ReadBoolean implements [Store].
|
|
func (s *EnvPolicyStore) ReadBoolean(key setting.Key) (bool, error) {
|
|
name, str, err := s.lookupSettingVariable(key)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if str == "" {
|
|
return false, setting.ErrNotConfigured
|
|
}
|
|
value, err := strconv.ParseBool(str)
|
|
if err != nil {
|
|
return false, fmt.Errorf("%s: %w: %q is not a valid bool", name, setting.ErrTypeMismatch, str)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
// ReadStringArray implements [Store].
|
|
func (s *EnvPolicyStore) ReadStringArray(key setting.Key) ([]string, error) {
|
|
_, str, err := s.lookupSettingVariable(key)
|
|
if err != nil || str == "" {
|
|
return nil, err
|
|
}
|
|
var dst int
|
|
res := strings.Split(str, ",")
|
|
for src := range res {
|
|
res[dst] = strings.TrimSpace(res[src])
|
|
if res[dst] != "" {
|
|
dst++
|
|
}
|
|
}
|
|
return res[0:dst], nil
|
|
}
|
|
|
|
func (s *EnvPolicyStore) lookupSettingVariable(key setting.Key) (name, value string, err error) {
|
|
name, err = keyToEnvVarName(key)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
value, ok := lookupEnv(name)
|
|
if !ok {
|
|
return name, "", setting.ErrNotConfigured
|
|
}
|
|
return name, value, nil
|
|
}
|
|
|
|
var (
|
|
errEmptyKey = errors.New("key must not be empty")
|
|
errInvalidKey = errors.New("key must consist of alphanumeric characters and slashes")
|
|
)
|
|
|
|
// keyToEnvVarName returns the environment variable name for a given policy
|
|
// setting key, or an error if the key is invalid. It converts CamelCase keys into
|
|
// underscore-separated words and prepends the variable name with the TS prefix.
|
|
// For example: AuthKey => TS_AUTH_KEY, ExitNodeAllowLANAccess => TS_EXIT_NODE_ALLOW_LAN_ACCESS, etc.
|
|
//
|
|
// It's fine to use this in [EnvPolicyStore] without caching variable names since it's not a hot path.
|
|
// [EnvPolicyStore] is not a [Changeable] policy store, so the conversion will only happen once.
|
|
func keyToEnvVarName(key setting.Key) (string, error) {
|
|
if len(key) == 0 {
|
|
return "", errEmptyKey
|
|
}
|
|
|
|
isLower := func(c byte) bool { return 'a' <= c && c <= 'z' }
|
|
isUpper := func(c byte) bool { return 'A' <= c && c <= 'Z' }
|
|
isLetter := func(c byte) bool { return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') }
|
|
isDigit := func(c byte) bool { return '0' <= c && c <= '9' }
|
|
|
|
words := make([]string, 0, 8)
|
|
words = append(words, "TS_DEBUGSYSPOLICY")
|
|
var currentWord strings.Builder
|
|
for i := 0; i < len(key); i++ {
|
|
c := key[i]
|
|
if c >= utf8.RuneSelf {
|
|
return "", errInvalidKey
|
|
}
|
|
|
|
var split bool
|
|
switch {
|
|
case isLower(c):
|
|
c -= 'a' - 'A' // make upper
|
|
split = currentWord.Len() > 0 && !isLetter(key[i-1])
|
|
case isUpper(c):
|
|
if currentWord.Len() > 0 {
|
|
prevUpper := isUpper(key[i-1])
|
|
nextLower := i < len(key)-1 && isLower(key[i+1])
|
|
split = !prevUpper || nextLower // split on case transition
|
|
}
|
|
case isDigit(c):
|
|
split = currentWord.Len() > 0 && !isDigit(key[i-1])
|
|
case c == setting.KeyPathSeparator:
|
|
words = append(words, currentWord.String())
|
|
currentWord.Reset()
|
|
continue
|
|
default:
|
|
return "", errInvalidKey
|
|
}
|
|
|
|
if split {
|
|
words = append(words, currentWord.String())
|
|
currentWord.Reset()
|
|
}
|
|
|
|
currentWord.WriteByte(c)
|
|
}
|
|
|
|
if currentWord.Len() > 0 {
|
|
words = append(words, currentWord.String())
|
|
}
|
|
|
|
return strings.Join(words, "_"), nil
|
|
}
|