506 lines
12 KiB
Go
506 lines
12 KiB
Go
// Package config provides the config values needed for Super Graph
|
|
// For detailed documentation visit https://supergraph.dev
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gobuffalo/flect"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
const (
|
|
LogLevelNone int = iota
|
|
LogLevelInfo
|
|
LogLevelWarn
|
|
LogLevelError
|
|
LogLevelDebug
|
|
)
|
|
|
|
// Config struct holds the Super Graph config values
|
|
type Config struct {
|
|
Core `mapstructure:",squash"`
|
|
Serv `mapstructure:",squash"`
|
|
|
|
vi *viper.Viper
|
|
log *log.Logger
|
|
logLevel int
|
|
roles map[string]*Role
|
|
abacEnabled bool
|
|
valid bool
|
|
}
|
|
|
|
// Core struct contains core specific config value
|
|
type Core struct {
|
|
Env string
|
|
Production bool
|
|
LogLevel string `mapstructure:"log_level"`
|
|
SecretKey string `mapstructure:"secret_key"`
|
|
SetUserID bool `mapstructure:"set_user_id"`
|
|
Vars map[string]string `mapstructure:"variables"`
|
|
Blocklist []string
|
|
Tables []Table
|
|
RolesQuery string `mapstructure:"roles_query"`
|
|
Roles []Role
|
|
}
|
|
|
|
// Serv struct contains config values used by the Super Graph service
|
|
type Serv struct {
|
|
AppName string `mapstructure:"app_name"`
|
|
HostPort string `mapstructure:"host_port"`
|
|
Host string
|
|
Port string
|
|
HTTPGZip bool `mapstructure:"http_compress"`
|
|
WebUI bool `mapstructure:"web_ui"`
|
|
EnableTracing bool `mapstructure:"enable_tracing"`
|
|
UseAllowList bool `mapstructure:"use_allow_list"`
|
|
WatchAndReload bool `mapstructure:"reload_on_config_change"`
|
|
AuthFailBlock bool `mapstructure:"auth_fail_block"`
|
|
SeedFile string `mapstructure:"seed_file"`
|
|
MigrationsPath string `mapstructure:"migrations_path"`
|
|
AllowedOrigins []string `mapstructure:"cors_allowed_origins"`
|
|
DebugCORS bool `mapstructure:"cors_debug"`
|
|
|
|
Inflections map[string]string
|
|
|
|
Auth Auth
|
|
Auths []Auth
|
|
|
|
DB struct {
|
|
Type string
|
|
Host string
|
|
Port uint16
|
|
DBName string
|
|
User string
|
|
Password string
|
|
Schema string
|
|
PoolSize int32 `mapstructure:"pool_size"`
|
|
MaxRetries int `mapstructure:"max_retries"`
|
|
PingTimeout time.Duration `mapstructure:"ping_timeout"`
|
|
} `mapstructure:"database"`
|
|
|
|
Actions []Action
|
|
}
|
|
|
|
// Auth struct contains authentication related config values used by the Super Graph service
|
|
type Auth struct {
|
|
Name string
|
|
Type string
|
|
Cookie string
|
|
CredsInHeader bool `mapstructure:"creds_in_header"`
|
|
|
|
Rails struct {
|
|
Version string
|
|
SecretKeyBase string `mapstructure:"secret_key_base"`
|
|
URL string
|
|
Password string
|
|
MaxIdle int `mapstructure:"max_idle"`
|
|
MaxActive int `mapstructure:"max_active"`
|
|
Salt string
|
|
SignSalt string `mapstructure:"sign_salt"`
|
|
AuthSalt string `mapstructure:"auth_salt"`
|
|
}
|
|
|
|
JWT struct {
|
|
Provider string
|
|
Secret string
|
|
PubKeyFile string `mapstructure:"public_key_file"`
|
|
PubKeyType string `mapstructure:"public_key_type"`
|
|
}
|
|
|
|
Header struct {
|
|
Name string
|
|
Value string
|
|
Exists bool
|
|
}
|
|
}
|
|
|
|
// Column struct defines a database column
|
|
type Column struct {
|
|
Name string
|
|
Type string
|
|
ForeignKey string `mapstructure:"related_to"`
|
|
}
|
|
|
|
// Table struct defines a database table
|
|
type Table struct {
|
|
Name string
|
|
Table string
|
|
Blocklist []string
|
|
Remotes []Remote
|
|
Columns []Column
|
|
}
|
|
|
|
// Remote struct defines a remote API endpoint
|
|
type Remote struct {
|
|
Name string
|
|
ID string
|
|
Path string
|
|
URL string
|
|
Debug bool
|
|
PassHeaders []string `mapstructure:"pass_headers"`
|
|
SetHeaders []struct {
|
|
Name string
|
|
Value string
|
|
} `mapstructure:"set_headers"`
|
|
}
|
|
|
|
// Query struct contains access control values for query operations
|
|
type Query struct {
|
|
Limit int
|
|
Filters []string
|
|
Columns []string
|
|
DisableFunctions bool `mapstructure:"disable_functions"`
|
|
Block bool
|
|
}
|
|
|
|
// Insert struct contains access control values for insert operations
|
|
type Insert struct {
|
|
Filters []string
|
|
Columns []string
|
|
Presets map[string]string
|
|
Block bool
|
|
}
|
|
|
|
// Insert struct contains access control values for update operations
|
|
type Update struct {
|
|
Filters []string
|
|
Columns []string
|
|
Presets map[string]string
|
|
Block bool
|
|
}
|
|
|
|
// Delete struct contains access control values for delete operations
|
|
type Delete struct {
|
|
Filters []string
|
|
Columns []string
|
|
Block bool
|
|
}
|
|
|
|
// RoleTable struct contains role specific access control values for a database table
|
|
type RoleTable struct {
|
|
Name string
|
|
|
|
Query Query
|
|
Insert Insert
|
|
Update Update
|
|
Delete Delete
|
|
}
|
|
|
|
// Role struct contains role specific access control values for for all database tables
|
|
type Role struct {
|
|
Name string
|
|
Match string
|
|
Tables []RoleTable
|
|
tablesMap map[string]*RoleTable
|
|
}
|
|
|
|
// Action struct contains config values for a Super Graph service action
|
|
type Action struct {
|
|
Name string
|
|
SQL string
|
|
AuthName string `mapstructure:"auth_name"`
|
|
}
|
|
|
|
// NewConfig function reads in the config file for the environment specified in the GO_ENV
|
|
// environment variable. This is the best way to create a new Super Graph config.
|
|
func NewConfig(path string) (*Config, error) {
|
|
return NewConfigWithLogger(path, log.New(os.Stdout, "", 0))
|
|
}
|
|
|
|
// NewConfigWithLogger function reads in the config file for the environment specified in the GO_ENV
|
|
// environment variable. This is the best way to create a new Super Graph config.
|
|
func NewConfigWithLogger(path string, logger *log.Logger) (*Config, error) {
|
|
vi := newViper(path, GetConfigName())
|
|
|
|
if err := vi.ReadInConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inherits := vi.GetString("inherits")
|
|
|
|
if len(inherits) != 0 {
|
|
vi = newViper(path, inherits)
|
|
|
|
if err := vi.ReadInConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if vi.IsSet("inherits") {
|
|
return nil, fmt.Errorf("inherited config (%s) cannot itself inherit (%s)",
|
|
inherits,
|
|
vi.GetString("inherits"))
|
|
}
|
|
|
|
vi.SetConfigName(GetConfigName())
|
|
|
|
if err := vi.MergeInConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
c := &Config{log: logger, vi: vi}
|
|
|
|
if err := vi.Unmarshal(&c); err != nil {
|
|
return nil, fmt.Errorf("failed to decode config, %v", err)
|
|
}
|
|
|
|
if err := c.init(); err != nil {
|
|
return nil, fmt.Errorf("failed to initialize config: %w", err)
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// NewConfigFrom function initializes a Config struct that you manually created
|
|
// so it can be used by Super Graph
|
|
func NewConfigFrom(c *Config, configPath string, logger *log.Logger) (*Config, error) {
|
|
c.vi = newViper(configPath, GetConfigName())
|
|
c.log = logger
|
|
if err := c.init(); err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func newViper(configPath, filename string) *viper.Viper {
|
|
vi := viper.New()
|
|
|
|
vi.SetEnvPrefix("SG")
|
|
vi.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
vi.AutomaticEnv()
|
|
|
|
vi.SetConfigName(filename)
|
|
vi.AddConfigPath(configPath)
|
|
vi.AddConfigPath("./config")
|
|
|
|
vi.SetDefault("host_port", "0.0.0.0:8080")
|
|
vi.SetDefault("web_ui", false)
|
|
vi.SetDefault("enable_tracing", false)
|
|
vi.SetDefault("auth_fail_block", "always")
|
|
vi.SetDefault("seed_file", "seed.js")
|
|
|
|
vi.SetDefault("database.type", "postgres")
|
|
vi.SetDefault("database.host", "localhost")
|
|
vi.SetDefault("database.port", 5432)
|
|
vi.SetDefault("database.user", "postgres")
|
|
vi.SetDefault("database.schema", "public")
|
|
|
|
vi.SetDefault("env", "development")
|
|
|
|
vi.BindEnv("env", "GO_ENV") //nolint: errcheck
|
|
vi.BindEnv("host", "HOST") //nolint: errcheck
|
|
vi.BindEnv("port", "PORT") //nolint: errcheck
|
|
|
|
vi.SetDefault("auth.rails.max_idle", 80)
|
|
vi.SetDefault("auth.rails.max_active", 12000)
|
|
|
|
return vi
|
|
}
|
|
|
|
func (c *Config) init() error {
|
|
switch c.Core.LogLevel {
|
|
case "debug":
|
|
c.logLevel = LogLevelDebug
|
|
case "error":
|
|
c.logLevel = LogLevelError
|
|
case "warn":
|
|
c.logLevel = LogLevelWarn
|
|
case "info":
|
|
c.logLevel = LogLevelInfo
|
|
default:
|
|
c.logLevel = LogLevelNone
|
|
}
|
|
|
|
if c.UseAllowList {
|
|
c.Production = true
|
|
}
|
|
|
|
for k, v := range c.Inflections {
|
|
flect.AddPlural(k, v)
|
|
}
|
|
|
|
// Tables: Validate and sanitize
|
|
tm := make(map[string]struct{})
|
|
|
|
for i := 0; i < len(c.Tables); i++ {
|
|
t := &c.Tables[i]
|
|
t.Name = flect.Pluralize(strings.ToLower(t.Name))
|
|
|
|
if _, ok := tm[t.Name]; ok {
|
|
c.Tables = append(c.Tables[:i], c.Tables[i+1:]...)
|
|
c.log.Printf("WRN duplicate table found: %s", t.Name)
|
|
}
|
|
tm[t.Name] = struct{}{}
|
|
|
|
t.Table = flect.Pluralize(strings.ToLower(t.Table))
|
|
}
|
|
|
|
// Variables: Validate and sanitize
|
|
for k, v := range c.Vars {
|
|
c.Vars[k] = sanitize(v)
|
|
}
|
|
|
|
// Roles: validate and sanitize
|
|
c.RolesQuery = sanitize(c.RolesQuery)
|
|
c.roles = make(map[string]*Role)
|
|
|
|
for i := 0; i < len(c.Roles); i++ {
|
|
r := &c.Roles[i]
|
|
r.Name = strings.ToLower(r.Name)
|
|
|
|
if _, ok := c.roles[r.Name]; ok {
|
|
c.Roles = append(c.Roles[:i], c.Roles[i+1:]...)
|
|
c.log.Printf("WRN duplicate role found: %s", r.Name)
|
|
}
|
|
|
|
r.Match = sanitize(r.Match)
|
|
r.tablesMap = make(map[string]*RoleTable)
|
|
|
|
for n, table := range r.Tables {
|
|
r.tablesMap[table.Name] = &r.Tables[n]
|
|
}
|
|
|
|
c.roles[r.Name] = r
|
|
}
|
|
|
|
if _, ok := c.roles["user"]; !ok {
|
|
u := Role{Name: "user"}
|
|
c.Roles = append(c.Roles, u)
|
|
c.roles["user"] = &u
|
|
}
|
|
|
|
if _, ok := c.roles["anon"]; !ok {
|
|
c.log.Printf("WRN unauthenticated requests will be blocked. no role 'anon' defined")
|
|
c.AuthFailBlock = true
|
|
}
|
|
|
|
if len(c.RolesQuery) == 0 {
|
|
c.log.Printf("WRN roles_query not defined: attribute based access control disabled")
|
|
}
|
|
|
|
if len(c.RolesQuery) == 0 {
|
|
c.abacEnabled = false
|
|
} else {
|
|
switch len(c.Roles) {
|
|
case 0, 1:
|
|
c.abacEnabled = false
|
|
case 2:
|
|
_, ok1 := c.roles["anon"]
|
|
_, ok2 := c.roles["user"]
|
|
c.abacEnabled = !(ok1 && ok2)
|
|
default:
|
|
c.abacEnabled = true
|
|
}
|
|
}
|
|
|
|
// Auths: validate and sanitize
|
|
am := make(map[string]struct{})
|
|
|
|
for i := 0; i < len(c.Auths); i++ {
|
|
a := &c.Auths[i]
|
|
a.Name = strings.ToLower(a.Name)
|
|
|
|
if _, ok := am[a.Name]; ok {
|
|
c.Auths = append(c.Auths[:i], c.Auths[i+1:]...)
|
|
c.log.Printf("WRN duplicate auth found: %s", a.Name)
|
|
}
|
|
am[a.Name] = struct{}{}
|
|
}
|
|
|
|
// Actions: validate and sanitize
|
|
axm := make(map[string]struct{})
|
|
|
|
for i := 0; i < len(c.Actions); i++ {
|
|
a := &c.Actions[i]
|
|
a.Name = strings.ToLower(a.Name)
|
|
a.AuthName = strings.ToLower(a.AuthName)
|
|
|
|
if _, ok := axm[a.Name]; ok {
|
|
c.Actions = append(c.Actions[:i], c.Actions[i+1:]...)
|
|
c.log.Printf("WRN duplicate action found: %s", a.Name)
|
|
}
|
|
|
|
if _, ok := am[a.AuthName]; !ok {
|
|
c.Actions = append(c.Actions[:i], c.Actions[i+1:]...)
|
|
c.log.Printf("WRN invalid auth_name '%s' for auth: %s", a.AuthName, a.Name)
|
|
}
|
|
axm[a.Name] = struct{}{}
|
|
}
|
|
|
|
c.valid = true
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetDBTableAliases function returns a map with database tables as keys
|
|
// and a list of aliases as values
|
|
func (c *Config) GetDBTableAliases() map[string][]string {
|
|
m := make(map[string][]string, len(c.Tables))
|
|
|
|
for i := range c.Tables {
|
|
t := c.Tables[i]
|
|
|
|
if len(t.Table) == 0 || len(t.Columns) != 0 {
|
|
continue
|
|
}
|
|
|
|
m[t.Table] = append(m[t.Table], t.Name)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// IsABACEnabled function returns true if attribute based access control is enabled
|
|
func (c *Config) IsABACEnabled() bool {
|
|
return c.abacEnabled
|
|
}
|
|
|
|
// IsAnonRoleDefined function returns true if the config has configuration for the `anon` role
|
|
func (c *Config) IsAnonRoleDefined() bool {
|
|
_, ok := c.roles["anon"]
|
|
return ok
|
|
}
|
|
|
|
// GetRole function returns returns the Role struct by name
|
|
func (c *Config) GetRole(name string) *Role {
|
|
role := c.roles[name]
|
|
return role
|
|
}
|
|
|
|
// ConfigPathUsed function returns the path to the current config file (excluding filename)
|
|
func (c *Config) ConfigPathUsed() string {
|
|
return path.Dir(c.vi.ConfigFileUsed())
|
|
}
|
|
|
|
// WriteConfigAs function writes the config to a file
|
|
// Format defined by extension (eg: .yml, .json)
|
|
func (c *Config) WriteConfigAs(fname string) error {
|
|
return c.vi.WriteConfigAs(fname)
|
|
}
|
|
|
|
// Log function returns the logger
|
|
func (c *Config) Log() *log.Logger {
|
|
return c.log
|
|
}
|
|
|
|
// LogLevel function returns the log level
|
|
func (c *Config) LogLevel() int {
|
|
return c.logLevel
|
|
}
|
|
|
|
// IsValid function returns true if the Config struct is initialized and valid
|
|
func (c *Config) IsValid() bool {
|
|
return c.valid
|
|
}
|
|
|
|
// GetTable function returns the RoleTable struct for a Role by table name
|
|
func (r *Role) GetTable(name string) *RoleTable {
|
|
table := r.tablesMap[name]
|
|
return table
|
|
}
|