2020-05-05 07:53:42 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/url"
|
2021-03-05 16:21:11 +00:00
|
|
|
"regexp"
|
2020-05-05 07:53:42 +00:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"sync/atomic"
|
|
|
|
|
2020-08-13 13:43:55 +00:00
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envtemplate"
|
2020-05-05 07:53:42 +00:00
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
|
|
|
"github.com/VictoriaMetrics/metrics"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2020-12-11 19:08:13 +00:00
|
|
|
authConfigPath = flag.String("auth.config", "", "Path to auth config. See https://victoriametrics.github.io/vmauth.html "+
|
2020-05-05 07:53:42 +00:00
|
|
|
"for details on the format of this auth config")
|
|
|
|
)
|
|
|
|
|
|
|
|
// AuthConfig represents auth config.
|
|
|
|
type AuthConfig struct {
|
|
|
|
Users []UserInfo `yaml:"users"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// UserInfo is user information read from authConfigPath
|
|
|
|
type UserInfo struct {
|
2021-02-11 10:40:59 +00:00
|
|
|
Username string `yaml:"username"`
|
|
|
|
Password string `yaml:"password"`
|
|
|
|
URLPrefix string `yaml:"url_prefix"`
|
|
|
|
URLMap []URLMap `yaml:"url_map"`
|
2020-05-05 07:53:42 +00:00
|
|
|
|
|
|
|
requests *metrics.Counter
|
|
|
|
}
|
|
|
|
|
2021-02-11 10:40:59 +00:00
|
|
|
// URLMap is a mapping from source paths to target urls.
|
|
|
|
type URLMap struct {
|
2021-03-05 16:21:11 +00:00
|
|
|
SrcPaths []*SrcPath `yaml:"src_paths"`
|
|
|
|
URLPrefix string `yaml:"url_prefix"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// SrcPath represents an src path
|
|
|
|
type SrcPath struct {
|
|
|
|
sOriginal string
|
|
|
|
re *regexp.Regexp
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sp *SrcPath) match(s string) bool {
|
|
|
|
prefix, ok := sp.re.LiteralPrefix()
|
|
|
|
if ok {
|
|
|
|
// Fast path - literal match
|
|
|
|
return s == prefix
|
|
|
|
}
|
|
|
|
if !strings.HasPrefix(s, prefix) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return sp.re.MatchString(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshalYAML implements yaml.Unmarshaler
|
|
|
|
func (sp *SrcPath) UnmarshalYAML(f func(interface{}) error) error {
|
|
|
|
var s string
|
|
|
|
if err := f(&s); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
sAnchored := "^(?:" + s + ")$"
|
|
|
|
re, err := regexp.Compile(sAnchored)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot build regexp from %q: %w", s, err)
|
|
|
|
}
|
|
|
|
sp.sOriginal = s
|
|
|
|
sp.re = re
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// MarshalYAML implements yaml.Marshaler.
|
|
|
|
func (sp *SrcPath) MarshalYAML() (interface{}, error) {
|
|
|
|
return sp.sOriginal, nil
|
2021-02-11 10:40:59 +00:00
|
|
|
}
|
|
|
|
|
2020-05-05 07:53:42 +00:00
|
|
|
func initAuthConfig() {
|
|
|
|
if len(*authConfigPath) == 0 {
|
2020-06-05 17:13:03 +00:00
|
|
|
logger.Fatalf("missing required `-auth.config` command-line flag")
|
2020-05-05 07:53:42 +00:00
|
|
|
}
|
|
|
|
m, err := readAuthConfig(*authConfigPath)
|
|
|
|
if err != nil {
|
2020-06-05 17:13:03 +00:00
|
|
|
logger.Fatalf("cannot load auth config from `-auth.config=%s`: %s", *authConfigPath, err)
|
2020-05-05 07:53:42 +00:00
|
|
|
}
|
|
|
|
authConfig.Store(m)
|
|
|
|
stopCh = make(chan struct{})
|
|
|
|
authConfigWG.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer authConfigWG.Done()
|
|
|
|
authConfigReloader()
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
func stopAuthConfig() {
|
|
|
|
close(stopCh)
|
|
|
|
authConfigWG.Wait()
|
|
|
|
}
|
|
|
|
|
|
|
|
func authConfigReloader() {
|
|
|
|
sighupCh := procutil.NewSighupChan()
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-stopCh:
|
|
|
|
return
|
|
|
|
case <-sighupCh:
|
2020-06-03 20:22:09 +00:00
|
|
|
logger.Infof("SIGHUP received; loading -auth.config=%q", *authConfigPath)
|
2020-05-05 07:53:42 +00:00
|
|
|
m, err := readAuthConfig(*authConfigPath)
|
|
|
|
if err != nil {
|
2020-06-03 20:22:09 +00:00
|
|
|
logger.Errorf("failed to load -auth.config=%q; using the last successfully loaded config; error: %s", *authConfigPath, err)
|
2020-05-05 07:53:42 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
authConfig.Store(m)
|
2020-06-03 20:22:09 +00:00
|
|
|
logger.Infof("Successfully reloaded -auth.config=%q", *authConfigPath)
|
2020-05-05 07:53:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var authConfig atomic.Value
|
|
|
|
var authConfigWG sync.WaitGroup
|
|
|
|
var stopCh chan struct{}
|
|
|
|
|
|
|
|
func readAuthConfig(path string) (map[string]*UserInfo, error) {
|
|
|
|
data, err := ioutil.ReadFile(path)
|
|
|
|
if err != nil {
|
2020-06-30 19:58:18 +00:00
|
|
|
return nil, fmt.Errorf("cannot read %q: %w", path, err)
|
2020-05-05 07:53:42 +00:00
|
|
|
}
|
|
|
|
m, err := parseAuthConfig(data)
|
|
|
|
if err != nil {
|
2020-06-30 19:58:18 +00:00
|
|
|
return nil, fmt.Errorf("cannot parse %q: %w", path, err)
|
2020-05-05 07:53:42 +00:00
|
|
|
}
|
|
|
|
logger.Infof("Loaded information about %d users from %q", len(m), path)
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseAuthConfig(data []byte) (map[string]*UserInfo, error) {
|
2020-08-13 13:43:55 +00:00
|
|
|
data = envtemplate.Replace(data)
|
2020-05-05 07:53:42 +00:00
|
|
|
var ac AuthConfig
|
|
|
|
if err := yaml.UnmarshalStrict(data, &ac); err != nil {
|
2020-06-30 19:58:18 +00:00
|
|
|
return nil, fmt.Errorf("cannot unmarshal AuthConfig data: %w", err)
|
2020-05-05 07:53:42 +00:00
|
|
|
}
|
|
|
|
uis := ac.Users
|
|
|
|
if len(uis) == 0 {
|
|
|
|
return nil, fmt.Errorf("`users` section cannot be empty in AuthConfig")
|
|
|
|
}
|
|
|
|
m := make(map[string]*UserInfo, len(uis))
|
|
|
|
for i := range uis {
|
|
|
|
ui := &uis[i]
|
|
|
|
if m[ui.Username] != nil {
|
|
|
|
return nil, fmt.Errorf("duplicate username found; username: %q", ui.Username)
|
|
|
|
}
|
2021-02-11 10:40:59 +00:00
|
|
|
if len(ui.URLPrefix) > 0 {
|
|
|
|
urlPrefix, err := sanitizeURLPrefix(ui.URLPrefix)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
ui.URLPrefix = urlPrefix
|
2020-05-05 07:53:42 +00:00
|
|
|
}
|
2021-02-11 10:40:59 +00:00
|
|
|
for _, e := range ui.URLMap {
|
|
|
|
if len(e.SrcPaths) == 0 {
|
|
|
|
return nil, fmt.Errorf("missing `src_paths`")
|
|
|
|
}
|
|
|
|
urlPrefix, err := sanitizeURLPrefix(e.URLPrefix)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
e.URLPrefix = urlPrefix
|
2020-05-05 07:53:42 +00:00
|
|
|
}
|
2021-02-11 10:40:59 +00:00
|
|
|
if len(ui.URLMap) == 0 && len(ui.URLPrefix) == 0 {
|
|
|
|
return nil, fmt.Errorf("missing `url_prefix`")
|
2020-05-05 07:53:42 +00:00
|
|
|
}
|
|
|
|
ui.requests = metrics.GetOrCreateCounter(fmt.Sprintf(`vmauth_user_requests_total{username=%q}`, ui.Username))
|
|
|
|
m[ui.Username] = ui
|
|
|
|
}
|
|
|
|
return m, nil
|
|
|
|
}
|
2021-02-11 10:40:59 +00:00
|
|
|
|
|
|
|
func sanitizeURLPrefix(urlPrefix string) (string, error) {
|
|
|
|
// Remove trailing '/' from urlPrefix
|
|
|
|
for strings.HasSuffix(urlPrefix, "/") {
|
|
|
|
urlPrefix = urlPrefix[:len(urlPrefix)-1]
|
|
|
|
}
|
|
|
|
// Validate urlPrefix
|
|
|
|
target, err := url.Parse(urlPrefix)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("invalid `url_prefix: %q`: %w", urlPrefix, err)
|
|
|
|
}
|
|
|
|
if target.Scheme != "http" && target.Scheme != "https" {
|
|
|
|
return "", fmt.Errorf("unsupported scheme for `url_prefix: %q`: %q; must be `http` or `https`", urlPrefix, target.Scheme)
|
|
|
|
}
|
|
|
|
if target.Host == "" {
|
|
|
|
return "", fmt.Errorf("missing hostname in `url_prefix %q`", urlPrefix)
|
|
|
|
}
|
|
|
|
return urlPrefix, nil
|
|
|
|
}
|