2019-04-12 16:12:13 +01:00
package tfa
import (
2019-04-18 15:07:39 +01:00
"bytes"
2019-04-12 16:12:13 +01:00
"encoding/json"
"errors"
"fmt"
2019-04-18 15:07:39 +01:00
"io"
"io/ioutil"
2019-04-12 16:12:13 +01:00
"os"
2019-04-18 15:07:39 +01:00
"regexp"
2019-04-17 11:29:35 +01:00
"strconv"
2019-04-12 16:12:13 +01:00
"strings"
"time"
2019-05-07 19:17:42 +01:00
"github.com/thomseddon/go-flags"
2019-04-12 16:12:13 +01:00
"github.com/thomseddon/traefik-forward-auth/internal/provider"
)
2019-09-18 17:55:52 +01:00
var config * Config
2019-04-17 11:29:35 +01:00
2020-05-11 14:42:33 +01:00
// Config holds the runtime application config
2019-04-12 16:12:13 +01:00
type Config struct {
2019-04-18 15:07:39 +01:00
LogLevel string ` long:"log-level" env:"LOG_LEVEL" default:"warn" choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" description:"Log level" `
LogFormat string ` long:"log-format" env:"LOG_FORMAT" default:"text" choice:"text" choice:"json" choice:"pretty" description:"Log format" `
2020-06-03 14:11:59 +01:00
AuthHost string ` long:"auth-host" env:"AUTH_HOST" description:"Single host to use when returning from 3rd party auth" `
Config func ( s string ) error ` long:"config" env:"CONFIG" description:"Path to config file" json:"-" `
CookieDomains [ ] CookieDomain ` long:"cookie-domain" env:"COOKIE_DOMAIN" env-delim:"," description:"Domain to set auth cookie on, can be set multiple times" `
InsecureCookie bool ` long:"insecure-cookie" env:"INSECURE_COOKIE" description:"Use insecure cookies" `
CookieName string ` long:"cookie-name" env:"COOKIE_NAME" default:"_forward_auth" description:"Cookie Name" `
CSRFCookieName string ` long:"csrf-cookie-name" env:"CSRF_COOKIE_NAME" default:"_forward_auth_csrf" description:"CSRF Cookie Name" `
DefaultAction string ` long:"default-action" env:"DEFAULT_ACTION" default:"auth" choice:"auth" choice:"allow" description:"Default action" `
DefaultProvider string ` long:"default-provider" env:"DEFAULT_PROVIDER" default:"google" choice:"google" choice:"oidc" description:"Default provider" `
Domains CommaSeparatedList ` long:"domain" env:"DOMAIN" env-delim:"," description:"Only allow given email domains, can be set multiple times" `
LifetimeString int ` long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds" `
LogoutRedirect string ` long:"logout-redirect" env:"LOGOUT_REDIRECT" description:"URL to redirect to following logout" `
MatchWhitelistOrDomain bool ` long:"match-whitelist-or-domain" env:"MATCH_WHITELIST_OR_DOMAIN" description:"Allow users that match *either* whitelist or domain (enabled by default in v3)" `
Path string ` long:"url-path" env:"URL_PATH" default:"/_oauth" description:"Callback URL Path" `
SecretString string ` long:"secret" env:"SECRET" description:"Secret used for signing (required)" json:"-" `
Whitelist CommaSeparatedList ` long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given email addresses, can be set multiple times" `
2019-04-18 15:07:39 +01:00
Providers provider . Providers ` group:"providers" namespace:"providers" env-namespace:"PROVIDERS" `
2020-02-10 17:01:01 +00:00
Rules map [ string ] * Rule ` long:"rule.<name>.<param>" description:"Rule definitions, param can be: \"action\", \"rule\" or \"provider\"" `
2019-04-18 15:07:39 +01:00
// Filled during transformations
2019-05-13 11:56:43 +01:00
Secret [ ] byte ` json:"-" `
2019-04-12 16:12:13 +01:00
Lifetime time . Duration
2019-04-18 15:07:39 +01:00
// Legacy
2019-06-11 13:14:29 +01:00
CookieDomainsLegacy CookieDomains ` long:"cookie-domains" env:"COOKIE_DOMAINS" description:"DEPRECATED - Use \"cookie-domain\"" `
CookieSecretLegacy string ` long:"cookie-secret" env:"COOKIE_SECRET" description:"DEPRECATED - Use \"secret\"" json:"-" `
CookieSecureLegacy string ` long:"cookie-secure" env:"COOKIE_SECURE" description:"DEPRECATED - Use \"insecure-cookie\"" `
2019-07-08 17:21:08 +01:00
ClientIdLegacy string ` long:"client-id" env:"CLIENT_ID" description:"DEPRECATED - Use \"providers.google.client-id\"" `
2019-06-11 13:14:29 +01:00
ClientSecretLegacy string ` long:"client-secret" env:"CLIENT_SECRET" description:"DEPRECATED - Use \"providers.google.client-id\"" json:"-" `
PromptLegacy string ` long:"prompt" env:"PROMPT" description:"DEPRECATED - Use \"providers.google.prompt\"" `
2019-04-12 16:12:13 +01:00
}
2020-05-11 14:42:33 +01:00
// NewGlobalConfig creates a new global config, parsed from command arguments
2019-09-18 17:55:52 +01:00
func NewGlobalConfig ( ) * Config {
2019-04-17 11:29:35 +01:00
var err error
config , err = NewConfig ( os . Args [ 1 : ] )
if err != nil {
2019-04-18 15:07:39 +01:00
fmt . Printf ( "%+v\n" , err )
2019-04-17 11:29:35 +01:00
os . Exit ( 1 )
}
return config
2019-04-12 16:12:13 +01:00
}
2019-09-18 17:55:52 +01:00
// TODO: move config parsing into new func "NewParsedConfig"
2020-05-11 14:42:33 +01:00
// NewConfig parses and validates provided configuration into a config object
2019-09-18 17:55:52 +01:00
func NewConfig ( args [ ] string ) ( * Config , error ) {
c := & Config {
2019-04-17 11:29:35 +01:00
Rules : map [ string ] * Rule { } ,
}
2019-04-12 16:12:13 +01:00
2019-04-17 11:29:35 +01:00
err := c . parseFlags ( args )
if err != nil {
return c , err
}
2019-04-12 16:12:13 +01:00
2019-04-18 15:07:39 +01:00
// TODO: as log flags have now been parsed maybe we should return here so
// any further errors can be logged via logrus instead of printed?
2019-09-18 17:55:52 +01:00
// TODO: Rename "Validate" method to "Setup" and move all below logic
// Setup
// Set default provider on any rules where it's not specified
for _ , rule := range c . Rules {
if rule . Provider == "" {
rule . Provider = c . DefaultProvider
}
}
2019-04-18 15:07:39 +01:00
// Backwards compatability
2019-04-23 19:16:24 +01:00
if c . CookieSecretLegacy != "" && c . SecretString == "" {
2019-05-07 11:39:24 +01:00
fmt . Println ( "cookie-secret config option is deprecated, please use secret" )
2019-04-23 19:16:24 +01:00
c . SecretString = c . CookieSecretLegacy
}
2019-04-18 15:07:39 +01:00
if c . ClientIdLegacy != "" {
2019-09-18 17:55:52 +01:00
c . Providers . Google . ClientID = c . ClientIdLegacy
2019-04-18 15:07:39 +01:00
}
if c . ClientSecretLegacy != "" {
c . Providers . Google . ClientSecret = c . ClientSecretLegacy
}
if c . PromptLegacy != "" {
2019-05-07 11:39:24 +01:00
fmt . Println ( "prompt config option is deprecated, please use providers.google.prompt" )
2019-04-18 15:07:39 +01:00
c . Providers . Google . Prompt = c . PromptLegacy
}
if c . CookieSecureLegacy != "" {
2019-05-07 11:39:24 +01:00
fmt . Println ( "cookie-secure config option is deprecated, please use insecure-cookie" )
2019-04-18 15:07:39 +01:00
secure , err := strconv . ParseBool ( c . CookieSecureLegacy )
if err != nil {
return c , err
}
c . InsecureCookie = ! secure
}
2019-04-23 18:26:56 +01:00
if len ( c . CookieDomainsLegacy ) > 0 {
2019-05-07 11:39:24 +01:00
fmt . Println ( "cookie-domains config option is deprecated, please use cookie-domain" )
2019-04-23 18:26:56 +01:00
c . CookieDomains = append ( c . CookieDomains , c . CookieDomainsLegacy ... )
}
2019-04-18 15:07:39 +01:00
2019-04-12 16:12:13 +01:00
// Transformations
2019-04-23 18:26:56 +01:00
if len ( c . Path ) > 0 && c . Path [ 0 ] != '/' {
c . Path = "/" + c . Path
}
2019-04-17 11:29:35 +01:00
c . Secret = [ ] byte ( c . SecretString )
c . Lifetime = time . Second * time . Duration ( c . LifetimeString )
2019-04-12 16:12:13 +01:00
2019-04-17 11:29:35 +01:00
return c , nil
2019-04-12 16:12:13 +01:00
}
2019-04-17 11:29:35 +01:00
func ( c * Config ) parseFlags ( args [ ] string ) error {
2019-05-13 11:56:43 +01:00
p := flags . NewParser ( c , flags . Default | flags . IniUnknownOptionHandler )
2019-04-18 15:07:39 +01:00
p . UnknownOptionHandler = c . parseUnknownFlag
i := flags . NewIniParser ( p )
c . Config = func ( s string ) error {
// Try parsing at as an ini
err := i . ParseFile ( s )
// If it fails with a syntax error, try converting legacy to ini
if err != nil && strings . Contains ( err . Error ( ) , "malformed key=value" ) {
converted , convertErr := convertLegacyToIni ( s )
if convertErr != nil {
// If conversion fails, return the original error
return err
}
2019-04-17 11:29:35 +01:00
2019-05-07 19:17:42 +01:00
fmt . Println ( "config format deprecated, please use ini format" )
2019-04-18 15:07:39 +01:00
return i . Parse ( converted )
2019-04-17 11:29:35 +01:00
}
2019-04-18 15:07:39 +01:00
return err
}
_ , err := p . ParseArgs ( args )
if err != nil {
2019-07-08 17:21:08 +01:00
return handleFlagError ( err )
2019-04-17 11:29:35 +01:00
}
return nil
}
func ( c * Config ) parseUnknownFlag ( option string , arg flags . SplitArgument , args [ ] string ) ( [ ] string , error ) {
// Parse rules in the format "rule.<name>.<param>"
parts := strings . Split ( option , "." )
if len ( parts ) == 3 && parts [ 0 ] == "rule" {
2019-06-10 11:37:38 +01:00
// Ensure there is a name
name := parts [ 1 ]
if len ( name ) == 0 {
return args , errors . New ( "route name is required" )
2019-04-17 11:29:35 +01:00
}
// Get value, or pop the next arg
val , ok := arg . Value ( )
2019-06-10 11:37:38 +01:00
if ! ok && len ( args ) > 1 {
2019-04-17 11:29:35 +01:00
val = args [ 0 ]
args = args [ 1 : ]
}
// Check value
if len ( val ) == 0 {
return args , errors . New ( "route param value is required" )
}
// Unquote if required
if val [ 0 ] == '"' {
var err error
val , err = strconv . Unquote ( val )
if err != nil {
return args , err
}
}
2019-06-10 11:37:38 +01:00
// Get or create rule
rule , ok := c . Rules [ name ]
if ! ok {
rule = NewRule ( )
c . Rules [ name ] = rule
}
2019-04-17 11:29:35 +01:00
// Add param value to rule
2019-04-18 15:07:39 +01:00
switch parts [ 2 ] {
2019-04-17 11:29:35 +01:00
case "action" :
rule . Action = val
case "rule" :
rule . Rule = val
case "provider" :
rule . Provider = val
default :
2019-07-08 17:21:08 +01:00
return args , fmt . Errorf ( "invalid route param: %v" , option )
2019-04-12 16:12:13 +01:00
}
2019-04-17 11:29:35 +01:00
} else {
return args , fmt . Errorf ( "unknown flag: %v" , option )
2019-04-12 16:12:13 +01:00
}
2019-04-17 11:29:35 +01:00
return args , nil
2019-04-12 16:12:13 +01:00
}
2019-07-08 17:21:08 +01:00
func handleFlagError ( err error ) error {
2019-04-18 15:07:39 +01:00
flagsErr , ok := err . ( * flags . Error )
if ok && flagsErr . Type == flags . ErrHelp {
// Library has just printed cli help
os . Exit ( 0 )
}
return err
}
2019-04-23 19:16:24 +01:00
var legacyFileFormat = regexp . MustCompile ( ` (?m)^([a-z-]+) (.*)$ ` )
2019-04-18 15:07:39 +01:00
func convertLegacyToIni ( name string ) ( io . Reader , error ) {
b , err := ioutil . ReadFile ( name )
if err != nil {
return nil , err
}
return bytes . NewReader ( legacyFileFormat . ReplaceAll ( b , [ ] byte ( "$1=$2" ) ) ) , nil
}
2020-05-11 14:42:33 +01:00
// Validate validates a config object
2019-04-17 11:29:35 +01:00
func ( c * Config ) Validate ( ) {
2019-04-12 16:12:13 +01:00
// Check for show stopper errors
if len ( c . Secret ) == 0 {
2019-09-18 17:55:52 +01:00
log . Fatal ( "\"secret\" option must be set" )
2019-04-12 16:12:13 +01:00
}
2019-09-18 17:55:52 +01:00
// Setup default provider
err := c . setupProvider ( c . DefaultProvider )
if err != nil {
log . Fatal ( err )
2019-04-12 16:12:13 +01:00
}
2019-04-17 11:29:35 +01:00
2019-09-18 17:55:52 +01:00
// Check rules (validates the rule and the rule provider)
2019-04-17 11:29:35 +01:00
for _ , rule := range c . Rules {
2019-09-18 17:55:52 +01:00
err = rule . Validate ( c )
if err != nil {
log . Fatal ( err )
}
2019-04-17 11:29:35 +01:00
}
2019-04-12 16:12:13 +01:00
}
2019-04-17 11:29:35 +01:00
func ( c Config ) String ( ) string {
2019-04-12 16:12:13 +01:00
jsonConf , _ := json . Marshal ( c )
return string ( jsonConf )
}
2019-04-17 11:29:35 +01:00
2019-09-18 17:55:52 +01:00
// GetProvider returns the provider of the given name
func ( c * Config ) GetProvider ( name string ) ( provider . Provider , error ) {
switch name {
case "google" :
return & c . Providers . Google , nil
case "oidc" :
return & c . Providers . OIDC , nil
}
return nil , fmt . Errorf ( "Unknown provider: %s" , name )
}
// GetConfiguredProvider returns the provider of the given name, if it has been
// configured. Returns an error if the provider is unknown, or hasn't been configured
func ( c * Config ) GetConfiguredProvider ( name string ) ( provider . Provider , error ) {
// Check the provider has been configured
if ! c . providerConfigured ( name ) {
return nil , fmt . Errorf ( "Unconfigured provider: %s" , name )
}
return c . GetProvider ( name )
}
func ( c * Config ) providerConfigured ( name string ) bool {
// Check default provider
if name == c . DefaultProvider {
return true
}
// Check rule providers
for _ , rule := range c . Rules {
if name == rule . Provider {
return true
}
}
return false
}
func ( c * Config ) setupProvider ( name string ) error {
// Check provider exists
p , err := c . GetProvider ( name )
if err != nil {
return err
}
// Setup
err = p . Setup ( )
if err != nil {
return err
}
return nil
}
2020-05-11 14:42:33 +01:00
// Rule holds defined rules
2019-04-17 11:29:35 +01:00
type Rule struct {
Action string
Rule string
Provider string
}
2020-05-11 14:42:33 +01:00
// NewRule creates a new rule object
2019-04-17 11:29:35 +01:00
func NewRule ( ) * Rule {
return & Rule {
2019-09-18 17:55:52 +01:00
Action : "auth" ,
2019-04-17 11:29:35 +01:00
}
}
2019-05-07 14:16:38 +01:00
func ( r * Rule ) formattedRule ( ) string {
// Traefik implements their own "Host" matcher and then offers "HostRegexp"
// to invoke the mux "Host" matcher. This ensures the mux version is used
return strings . ReplaceAll ( r . Rule , "Host(" , "HostRegexp(" )
}
2020-05-11 14:42:33 +01:00
// Validate validates a rule
2019-09-18 17:55:52 +01:00
func ( r * Rule ) Validate ( c * Config ) error {
2019-04-17 11:29:35 +01:00
if r . Action != "auth" && r . Action != "allow" {
2019-09-18 17:55:52 +01:00
return errors . New ( "invalid rule action, must be \"auth\" or \"allow\"" )
2019-04-17 11:29:35 +01:00
}
2019-09-18 17:55:52 +01:00
return c . setupProvider ( r . Provider )
2019-04-17 11:29:35 +01:00
}
2019-04-23 18:26:56 +01:00
// Legacy support for comma separated lists
2020-05-11 14:42:33 +01:00
// CommaSeparatedList provides legacy support for config values provided as csv
2019-04-17 11:29:35 +01:00
type CommaSeparatedList [ ] string
2020-05-11 14:42:33 +01:00
// UnmarshalFlag converts a comma separated list to an array
2019-04-17 11:29:35 +01:00
func ( c * CommaSeparatedList ) UnmarshalFlag ( value string ) error {
2019-04-23 18:26:56 +01:00
* c = append ( * c , strings . Split ( value , "," ) ... )
2019-04-17 11:29:35 +01:00
return nil
}
2020-05-11 14:42:33 +01:00
// MarshalFlag converts an array back to a comma separated list
2019-04-17 11:29:35 +01:00
func ( c * CommaSeparatedList ) MarshalFlag ( ) ( string , error ) {
return strings . Join ( * c , "," ) , nil
}