Files
glauth/glauth.go
T
Jörn Friedrich Dreyer 592fdf4e79 use functional options pattern to inject logr (#124)
* use functional options pattern to inject logr

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* cleanup log formatting

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2020-02-26 15:01:31 +01:00

386 lines
10 KiB
Go

package main
import (
"fmt"
"os"
"strings"
"github.com/BurntSushi/toml"
"github.com/GeertJohan/yubigo"
docopt "github.com/docopt/docopt-go"
"github.com/fsnotify/fsnotify"
"github.com/glauth/glauth/pkg/config"
"github.com/glauth/glauth/pkg/frontend"
gologgingr "github.com/glauth/glauth/pkg/gologgingr"
"github.com/glauth/glauth/pkg/server"
"github.com/glauth/glauth/pkg/stats"
"github.com/go-logr/logr"
"github.com/jinzhu/copier"
logging "github.com/op/go-logging"
"gopkg.in/amz.v1/aws"
"gopkg.in/amz.v1/s3"
)
// Set with buildtime vars
var LastGitTag string
var BuildTime string
var GitCommit string
var GitClean string
var GitBranch string
var GitTagIsCommit string
const programName = "glauth"
var usage = `glauth: securely expose your LDAP for external auth
Usage:
glauth [options] -c <file|s3 url>
glauth -h --help
glauth --version
Options:
-c, --config <file> Config file.
-K <aws_key_id> AWS Key ID.
-S <aws_secret_key> AWS Secret Key.
-r <aws_region> AWS Region [default: us-east-1].
-h, --help Show this screen.
--version Show version.
`
var (
log logr.Logger
args map[string]interface{}
stderr *logging.LogBackend
yubiAuth *yubigo.YubiAuth
activeConfig = &config.Config{}
)
// Reads builtime vars and returns a full string containing info about
// the currently running version of the software. Primarily used by the
// --version flag at runtime.
func getVersionString() string {
var versionstr string
versionstr = "GLauth"
// Notate the git context of the build
switch {
// If a release, use the tag
case GitClean == "1" && GitTagIsCommit == "1":
versionstr += " " + LastGitTag + "\n\n"
// If this branch had a tag before, mention the branch and the tag to give a rough idea of the base version
case len(GitBranch) > 1 && len(LastGitTag) > 1:
versionstr += "\nNon-release build from branch " + GitBranch + ", based on tag " + LastGitTag + "\n\n"
// If no previous tag specified, just mention the branch
case len(GitBranch) > 1:
versionstr += "\nNon-release build from branch " + GitBranch + "\n\n"
// Fallback message, if all else fails
default:
versionstr += "\nNon-release build\n\n"
}
// Include build time
if len(BuildTime) > 1 {
versionstr += "Build time: " + BuildTime + "\n"
}
// Add commit hash
if GitClean == "1" && len(GitCommit) > 1 {
versionstr += "Commit: " + GitCommit + "\n"
}
return versionstr
}
func main() {
stderr = initLogging()
log.V(6).Info("AP start")
if err := parseArgs(); err != nil {
log.Error(err, "Could not parse command-line arguments")
os.Exit(1)
}
if err := doConfig(); err != nil {
log.Error(err, "Configuration file error")
os.Exit(1)
}
startService()
}
func startService() {
// stats
stats.General.Set("version", stats.Stringer(LastGitTag))
// web API
if activeConfig.API.Enabled {
log.V(6).Info("Web API enabled")
go frontend.RunAPI(
frontend.Logger(log),
frontend.Config(&activeConfig.API),
)
}
startConfigWatcher()
s, err := server.NewServer(
server.Logger(log),
server.Config(activeConfig),
)
if err != nil {
log.Error(err, "Could not start server")
os.Exit(1)
}
s.ListenAndServe()
log.V(0).Info("AP exit")
os.Exit(1)
}
func startConfigWatcher() {
configFileLocation := getConfigLocation()
if strings.HasPrefix(configFileLocation, "s3://") {
return
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Error(err, "Could not start config-watcher")
}
go func() {
for {
select {
case event := <-watcher.Events:
if activeConfig.WatchConfig {
if event.Op.String() == "WRITE" {
if err := doConfig(); err != nil {
log.V(2).Info("Could not reload config.Holding on to old config", "error", err.Error())
} else {
log.V(3).Info("Config was reloaded")
}
}
}
case err := <-watcher.Errors:
if activeConfig.WatchConfig {
log.Error(err, "Error!")
}
}
}
}()
watcher.Add(configFileLocation)
}
func parseArgs() error {
var err error
if args, err = docopt.Parse(usage, nil, true, getVersionString(), false); err != nil {
return err
}
return nil
}
func getConfigLocation() string {
return args["--config"].(string)
}
func parseConfigFile(configFileLocation string) (*config.Config, error) {
cfg := config.Config{}
// setup defaults
cfg.LDAP.Enabled = false
cfg.LDAPS.Enabled = true
cfg.Backend.NameFormat = "cn"
cfg.Backend.GroupFormat = "ou"
cfg.Backend.SSHKeyAttr = "sshPublicKey"
// parse the config file
if strings.HasPrefix(configFileLocation, "s3://") {
if _, present := aws.Regions[args["-r"].(string)]; present == false {
return &cfg, fmt.Errorf("Invalid AWS region: %s", args["-r"])
}
region := aws.Regions[args["-r"].(string)]
auth, err := aws.EnvAuth()
if err != nil {
if args["-K"] == nil || args["-S"] == nil {
return &cfg, fmt.Errorf("AWS credentials not found: must use -K and -S flags, or set these env vars:\n\texport AWS_ACCESS_KEY_ID=\"AAA...\"\n\texport AWS_SECRET_ACCESS_KEY=\"BBBB...\"\n")
}
auth = aws.Auth{
AccessKey: args["-K"].(string),
SecretKey: args["-S"].(string),
}
}
// parse S3 url
s3url := strings.TrimPrefix(configFileLocation, "s3://")
parts := strings.SplitN(s3url, "/", 2)
if len(parts) != 2 {
return &cfg, fmt.Errorf("Invalid S3 URL: %s", s3url)
}
b := s3.New(auth, region).Bucket(parts[0])
tomlData, err := b.Get(parts[1])
if err != nil {
return &cfg, err
}
if _, err := toml.Decode(string(tomlData), &cfg); err != nil {
return &cfg, err
}
} else { // local config file
if _, err := toml.DecodeFile(configFileLocation, &cfg); err != nil {
return &cfg, err
}
}
return &cfg, nil
}
func handleConfig(cfg config.Config) (*config.Config, error) {
if len(cfg.Frontend.Listen) > 0 && (len(cfg.LDAP.Listen) > 0 || len(cfg.LDAPS.Listen) > 0) {
// Both old server-config and new - dont allow
return &cfg, fmt.Errorf("Both old and new server-config in use - please remove old format ([frontend]) and migrate to new format ([ldap], [ldaps])")
}
if len(cfg.Frontend.Listen) > 0 {
// We're going with old format - parse it into new
log.V(2).Info("Config [frontend] is deprecated - please move to [ldap] and [ldaps] as-per documentation")
cfg.LDAP.Enabled = !cfg.Frontend.TLS
cfg.LDAPS.Enabled = cfg.Frontend.TLS
if cfg.Frontend.TLS {
cfg.LDAPS.Listen = cfg.Frontend.Listen
} else {
cfg.LDAP.Listen = cfg.Frontend.Listen
}
if len(cfg.Frontend.Cert) > 0 {
cfg.LDAPS.Cert = cfg.Frontend.Cert
}
if len(cfg.Frontend.Key) > 0 {
cfg.LDAPS.Key = cfg.Frontend.Key
}
}
if !cfg.LDAP.Enabled && !cfg.LDAPS.Enabled {
return &cfg, fmt.Errorf("No server configuration found: please provide either LDAP or LDAPS configuration")
}
if cfg.LDAPS.Enabled {
// LDAPS enabled - verify requirements (cert, key, listen)
if len(cfg.LDAPS.Cert) == 0 || len(cfg.LDAPS.Key) == 0 {
return &cfg, fmt.Errorf("LDAPS was enabled but no certificate or key were specified: please disable LDAPS or use the 'cert' and 'key' options")
}
if len(cfg.LDAPS.Listen) == 0 {
return &cfg, fmt.Errorf("No LDAPS bind address was specified: please disable LDAPS or use the 'listen' option")
}
}
if cfg.LDAP.Enabled {
// LDAP enabled - verify listen
if len(cfg.LDAP.Listen) == 0 {
return &cfg, fmt.Errorf("No LDAP bind address was specified: please disable LDAP or use the 'listen' option")
}
}
switch cfg.Backend.Datastore {
case "":
cfg.Backend.Datastore = "config"
case "config":
case "ldap":
case "owncloud":
default:
return &cfg, fmt.Errorf("invalid backend %s - must be 'config', 'ldap' or 'owncloud", cfg.Backend.Datastore)
}
return &cfg, nil
}
// doConfig reads the cli flags and config file
func doConfig() error {
// Parse config-file into config{} struct
cfg, err := parseConfigFile(getConfigLocation())
if err != nil {
return err
}
// Handle validation and parsing of old [frontend] section into [ldap] and/or [ldaps] sections
cfg, err = handleConfig(*cfg)
if err != nil {
return err
}
// Before greenlighting new config entirely, lets make sure the yubiauth works - in case they changed
if activeConfig.YubikeyClientID != cfg.YubikeyClientID || activeConfig.YubikeySecret != cfg.YubikeySecret {
if len(cfg.YubikeyClientID) > 0 && len(cfg.YubikeySecret) > 0 {
_yubiAuth, err := yubigo.NewYubiAuth(cfg.YubikeyClientID, cfg.YubikeySecret)
if err != nil {
return err
}
// No errors, override
yubiAuth = _yubiAuth
}
}
// All config is validated and alright, copy to ativeConfig
if err := copier.Copy(activeConfig, cfg); err != nil {
return err
}
// Handle logging settings for new config
// - we do this last to make sure we only respect a fully validated config
stderr = initLogging()
if activeConfig.Debug {
logging.SetLevel(logging.DEBUG, programName)
log.V(6).Info("Debugging enabled")
}
if activeConfig.Syslog {
enableSyslog(stderr)
}
return nil
}
// initLogging sets up logging to stderr
func initLogging() *logging.LogBackend {
l := logging.MustGetLogger(programName)
l.ExtraCalldepth = 2 // add extra call depth for the logr wrapper
log = gologgingr.New(
gologgingr.Logger(l),
)
gologgingr.SetVerbosity(10) // do not filter by verbosity. glauth uses the go-logging lib to filter the levels
format := "%{color}%{time:15:04:05.000000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}"
logBackend := logging.NewLogBackend(os.Stderr, "", 0)
logging.SetBackend(logBackend)
logging.SetLevel(logging.NOTICE, programName)
logging.SetFormatter(logging.MustStringFormatter(format))
return logBackend
}
// enableSyslog turns on syslog and turns off color
func enableSyslog(stderrBackend *logging.LogBackend) {
format := "%{time:15:04:05.000000} %{shortfunc} ▶ %{level:.4s} %{id:03x} %{message}"
logging.SetFormatter(logging.MustStringFormatter(format))
syslogBackend, err := logging.NewSyslogBackend("")
if err != nil {
log.Error(err, "Could not create new syslog backend")
os.Exit(1)
}
logging.SetBackend(stderrBackend, syslogBackend)
log.V(6).Info("Syslog enabled")
}