Skip to content
Snippets Groups Projects
config.go 7.13 KiB
Newer Older
  • Learn to ignore specific revisions
  • Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    package config
    
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    import (
    	"fmt"
    	"os"
    	"path/filepath"
    	"strings"
    
    	"github.com/coreos/go-oidc/v3/oidc"
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	log "github.com/sirupsen/logrus"
    
    	"github.com/zachmann/mytoken/internal/context"
    
    	"github.com/zachmann/mytoken/internal/model"
    	"github.com/zachmann/mytoken/internal/utils/fileutil"
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	"github.com/zachmann/mytoken/internal/utils/issuerUtils"
    
    	"github.com/zachmann/mytoken/pkg/oauth2x"
    	"gopkg.in/yaml.v3"
    
    var defaultConfig = Config{
    	Server: serverConf{
    		Port: 443,
    	},
    	DB: dbConf{
    		Host:     "localhost",
    		User:     "mytoken",
    		Password: "mytoken",
    		DB:       "mytoken",
    	},
    	Signing: signingConf{
    		Alg:       oidc.ES512,
    		RSAKeyLen: 2048,
    	},
    	Logging: loggingConf{
    		Access: LoggerConf{
    			Dir:    "/var/log/mytoken",
    			StdErr: false,
    		},
    		Internal: LoggerConf{
    			Dir:    "/var/log/mytoken",
    			StdErr: false,
    			Level:  "error",
    		},
    	},
    	ServiceDocumentation: "https://github.com/zachmann/mytoken",
    	Features: featuresConf{
    		EnabledOIDCFlows: []model.OIDCFlow{
    			model.OIDCFlowAuthorizationCode,
    		},
    		TokenRevocation: onlyEnable{true},
    
    		ShortTokens: shortTokenConfig{
    			Enabled: true,
    			Len:     64,
    		},
    		TransferCodes: transferCodeConfig{
    			Enabled:      true,
    			Len:          8,
    			ExpiresAfter: 300,
    		},
    
    		Polling: pollingConf{
    			Enabled:                 true,
    			PollingCodeExpiresAfter: 300,
    			PollingInterval:         5,
    		},
    		AccessTokenGrant: onlyEnable{true},
    		SignedJWTGrant:   onlyEnable{true},
    	},
    	ProviderByIssuer: make(map[string]*ProviderConf),
    }
    
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    // Config holds the server configuration
    type Config struct {
    
    	IssuerURL            string                   `yaml:"issuer"`
    	Server               serverConf               `yaml:"server"`
    	DB                   dbConf                   `yaml:"database"`
    	Signing              signingConf              `yaml:"signing"`
    	Logging              loggingConf              `yaml:"logging"`
    	ServiceDocumentation string                   `yaml:"service_documentation"`
    	Features             featuresConf             `yaml:"features"`
    	Providers            []*ProviderConf          `yaml:"providers"`
    	ProviderByIssuer     map[string]*ProviderConf `yaml:"-"`
    }
    
    type featuresConf struct {
    
    	EnabledOIDCFlows []model.OIDCFlow   `yaml:"enabled_oidc_flows"`
    	TokenRevocation  onlyEnable         `yaml:"token_revocation"`
    	ShortTokens      shortTokenConfig   `yaml:"short_tokens"`
    	TransferCodes    transferCodeConfig `yaml:"transfer_codes"`
    	Polling          pollingConf        `yaml:"polling_codes"`
    	AccessTokenGrant onlyEnable         `yaml:"access_token_grant"`
    	SignedJWTGrant   onlyEnable         `yaml:"signed_jwt_grant"`
    }
    
    type shortTokenConfig struct {
    	Enabled bool `yaml:"enabled"`
    	Len     int  `yaml:"len"`
    }
    
    type transferCodeConfig struct {
    	Enabled      bool `yaml:"enabled"`
    	Len          int  `yaml:"len"`
    	ExpiresAfter int  `yaml:"expires_after"`
    
    }
    
    type onlyEnable struct {
    	Enabled bool `yaml:"enabled"`
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    }
    
    type loggingConf struct {
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	Access   LoggerConf `yaml:"access"`
    	Internal LoggerConf `yaml:"internal"`
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    type LoggerConf struct {
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	Dir    string `yaml:"dir"`
    	StdErr bool   `yaml:"stderr"`
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	Level  string `yaml:"level"`
    
    }
    
    type pollingConf struct {
    
    	Enabled                 bool  `yaml:"enabled"`
    
    	PollingCodeExpiresAfter int64 `yaml:"expires_after"`
    	PollingInterval         int64 `yaml:"polling_interval"`
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    }
    
    type dbConf struct {
    	Host     string `yaml:"host"`
    	User     string `yaml:"user"`
    	Password string `yaml:"password"`
    	DB       string `yaml:"db"`
    }
    
    type serverConf struct {
    	Hostname string `yaml:"hostname"`
    
    	Port     int    `yaml:"port"`
    
    type signingConf struct {
    
    	Alg       string `yaml:"alg"`
    	KeyFile   string `yaml:"key_file"`
    	RSAKeyLen int    `yaml:"rsa_key_len"`
    
    // ProviderConf holds information about a provider
    type ProviderConf struct {
    
    	Issuer       string             `yaml:"issuer"`
    	ClientID     string             `yaml:"client_id"`
    	ClientSecret string             `yaml:"client_secret"`
    	Scopes       []string           `yaml:"scopes"`
    	Endpoints    *oauth2x.Endpoints `yaml:"-"`
    	Provider     *oidc.Provider     `yaml:"-"`
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    }
    
    var conf *Config
    
    // Get returns the config
    func Get() *Config {
    	return conf
    }
    
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    func validate() error {
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	if conf == nil {
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    		return fmt.Errorf("config not set")
    	}
    	if conf.Server.Hostname == "" {
    		return fmt.Errorf("invalid config: server.hostname not set")
    	}
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	if len(conf.Providers) <= 0 {
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    		return fmt.Errorf("invalid config: providers must have at least one entry")
    	}
    	for i, p := range conf.Providers {
    		if p.Issuer == "" {
    			return fmt.Errorf("invalid config: provider.issuer not set (Index %d)", i)
    		}
    
    		p.Endpoints, err = oauth2x.NewConfig(context.Get(), p.Issuer).Endpoints()
    
    		if err != nil {
    			return fmt.Errorf("Error '%s' for provider.issuer '%s' (Index %d)", err, p.Issuer, i)
    		}
    
    		p.Provider, err = oidc.NewProvider(context.Get(), p.Issuer)
    
    		if err != nil {
    			return fmt.Errorf("Error '%s' for provider.issuer '%s' (Index %d)", err, p.Issuer, i)
    		}
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    		if p.ClientID == "" {
    			return fmt.Errorf("invalid config: provider.clientid not set (Index %d)", i)
    		}
    		if p.ClientSecret == "" {
    			return fmt.Errorf("invalid config: provider.clientsecret not set (Index %d)", i)
    		}
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    		if len(p.Scopes) <= 0 {
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    			return fmt.Errorf("invalid config: provider.scopes not set (Index %d)", i)
    		}
    
    		iss0, iss1 := issuerUtils.GetIssuerWithAndWithoutSlash(p.Issuer)
    		conf.ProviderByIssuer[iss0] = p
    		conf.ProviderByIssuer[iss1] = p
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	}
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	if conf.IssuerURL == "" {
    		return fmt.Errorf("invalid config: issuerurl not set")
    	}
    
    	if conf.Signing.KeyFile == "" {
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    		return fmt.Errorf("invalid config: signingkeyfile not set")
    	}
    
    	if conf.Signing.Alg == "" {
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    		return fmt.Errorf("invalid config: tokensigningalg not set")
    	}
    
    	model.OIDCFlowAuthorizationCode.AddToSliceIfNotFound(conf.Features.EnabledOIDCFlows)
    	if model.OIDCFlowIsInSlice(model.OIDCFlowDevice, conf.Features.EnabledOIDCFlows) && conf.Features.Polling.Enabled == false {
    		return fmt.Errorf("oidc flow device flow requires polling_codes to be enabled")
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	return nil
    }
    
    var possibleConfigLocations = []string{
    	"config",
    	"/etc/mytoken",
    }
    
    // readConfigFile checks if a file exists in one of the configuration
    // directories and returns the content. If no file is found, mytoken exists.
    func readConfigFile(filename string) []byte {
    	for _, dir := range possibleConfigLocations {
    		filep := filepath.Join(dir, filename)
    		if strings.HasPrefix(filep, "~") {
    			homeDir := os.Getenv("HOME")
    			filep = filepath.Join(homeDir, filep[1:])
    		}
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    		log.WithField("filepath", filep).Debug("Looking for config file")
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    		if fileutil.FileExists(filep) {
    			return fileutil.MustReadFile(filep)
    		}
    	}
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	log.WithField("filepath", filename).Fatal("Could not find config file in any of the possible directories")
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	return nil
    }
    
    // Load reads the config file and populates the config struct; then validates the config
    func Load() {
    
    	load()
    	if err := validate(); err != nil {
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    		log.WithError(err).Fatal()
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	data := readConfigFile("config.yaml")
    
    	conf = &defaultConfig
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    	err := yaml.Unmarshal(data, conf)
    	if err != nil {
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    		log.WithError(err).Fatal()
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    		return
    	}
    
    }
    
    // LoadForSetup reads the config file and populates the config struct; it does not validate the config, since this is not required for setup
    func LoadForSetup() {
    	load()
    
    Gabriel Zachmann's avatar
    Gabriel Zachmann committed
    }