diff --git a/server/authserver.go b/server/authserver.go index 9c144d92939acd74e34d90a646297dc6cfac6bcd..27da3f0a854259cdfc88d30cb51f74260f003239 100644 --- a/server/authserver.go +++ b/server/authserver.go @@ -4,16 +4,12 @@ import ( "context" "errors" "fmt" - "io/ioutil" "log" - "path/filepath" "github.com/pquerna/otp/totp" "github.com/prometheus/client_golang/prometheus" "github.com/tstranex/u2f" - "gopkg.in/yaml.v2" - "git.autistici.org/ai3/go-common/clientutil" "git.autistici.org/ai3/go-common/pwhash" "git.autistici.org/id/auth" ) @@ -63,12 +59,16 @@ func (u *User) UserInfo() *auth.UserInfo { } } -// UserBackend provides us with per-service user information. -type UserBackend interface { +// userBackend provides us with per-service user information. +type userBackend interface { Close() NewServiceBackend(*BackendSpec) (serviceBackend, error) } +type serviceBackend interface { + GetUser(context.Context, string) (*User, bool) +} + // OTPShortTermStorage stores short-term otp tokens for replay // protection purposes. type OTPShortTermStorage interface { @@ -87,26 +87,6 @@ type requestFilter interface { Filter(*User, *auth.Request, *auth.Response) *auth.Response } -// BackendSpec specifies backend-specific configuration for a service. -type BackendSpec struct { - BackendName string `yaml:"backend"` - Params yaml.MapSlice `yaml:"params"` - StaticGroups []string `yaml:"static_groups"` -} - -// ServiceConfig defines the authentication backends for a service. -type ServiceConfig struct { - BackendSpecs []*BackendSpec `yaml:"backends"` - ChallengeResponse bool `yaml:"challenge_response"` - Enforce2FA bool `yaml:"enforce_2fa"` - EnableDeviceTracking bool `yaml:"enable_device_tracking"` - Ratelimits []string `yaml:"rate_limits"` -} - -type serviceBackend interface { - GetUser(context.Context, string) (*User, bool) -} - // Authenticate users for a specific service. type service struct { rl []*authRatelimiter @@ -158,38 +138,11 @@ func (b *staticGroupsServiceBackend) GetUser(ctx context.Context, name string) ( return user, ok } -// Config for the authentication server. -type Config struct { - // Service-specific configuration. - Services map[string]*ServiceConfig `yaml:"services"` - - // If set, load more service definitions from *.yml files in this directory. - ServicesDir string `yaml:"services_dir"` - - // Enabled backends. - Backends map[string]yaml.MapSlice `yaml:"enabled_backends"` - - // If set, load more backend definitions from *.yml files in this directory. - BackendsDir string `yaml:"backends_dir"` - - // Named rate limiter configurations. - RateLimiters map[string]*authRatelimiterConfig `yaml:"rate_limits"` - - // Configuration for the user-meta-server backend. - UserMetaDBConfig *clientutil.BackendConfig `yaml:"user_meta_server"` - - // Memcache servers used for short-term storage. - MemcacheServers []string `yaml:"memcache_servers"` - - // Full path to the configuration file, used to resolve relative paths. - path string -} - // Create backend implementations from the given Config. -func createBackends(config *Config) (map[string]UserBackend, error) { - backends := make(map[string]UserBackend) +func createBackends(config *Config) (map[string]userBackend, error) { + backends := make(map[string]userBackend) for name, params := range config.Backends { - var b UserBackend + var b userBackend var err error switch name { case "file": @@ -208,7 +161,7 @@ func createBackends(config *Config) (map[string]UserBackend, error) { } // Create a service backend from the given BackendSpec. -func createServiceBackend(spec *BackendSpec, backends map[string]UserBackend) (serviceBackend, error) { +func createServiceBackend(spec *BackendSpec, backends map[string]userBackend) (serviceBackend, error) { b, ok := backends[spec.BackendName] if !ok { return nil, fmt.Errorf("unknown backend %s", spec.BackendName) @@ -224,7 +177,7 @@ func createServiceBackend(spec *BackendSpec, backends map[string]UserBackend) (s } // Create a service from the given ServiceConfig. -func createService(config *Config, sc *ServiceConfig, backends map[string]UserBackend, ratelimiters map[string]*authRatelimiter, blacklists map[string]*authBlacklist) (*service, error) { +func createService(config *Config, sc *ServiceConfig, backends map[string]userBackend, ratelimiters map[string]*authRatelimiter, blacklists map[string]*authBlacklist) (*service, error) { s := &service{ enforce2FA: sc.Enforce2FA, challengeResponse: sc.ChallengeResponse, @@ -295,96 +248,10 @@ func createRatelimiters(config *Config) (map[string]*authRatelimiter, map[string return ratelimiters, blacklists, nil } -// Load and unmarshal a YAML file. -func loadYAML(path string, obj interface{}) error { - data, err := ioutil.ReadFile(path) // #nosec - if err != nil { - return err - } - return yaml.Unmarshal(data, obj) -} - -// Load a standalone service configuration: a YAML-encoded file that -// may contain one or more ServiceConfig definitions. -func loadStandaloneServiceConfig(path string) (map[string]*ServiceConfig, error) { - var out map[string]*ServiceConfig - if err := loadYAML(path, &out); err != nil { - return nil, err - } - return out, nil -} - -// Load a standalone service configuration: a YAML-encoded file that -// may contain one or more ServiceConfig definitions. -func loadStandaloneBackendConfig(path string) (map[string]yaml.MapSlice, error) { - var out map[string]yaml.MapSlice - if err := loadYAML(path, &out); err != nil { - return nil, err - } - return out, nil -} - -// Apply a function to all files matching a glob pattern (errors are ignored). -func forAllFiles(pattern string, f func(string)) { - files, err := filepath.Glob(pattern) - if err != nil { - return - } - for _, file := range files { - f(file) - } -} - -// LoadConfig loads the configuration from a YAML-encoded file. -func LoadConfig(path string) (*Config, error) { - config := Config{ - path: path, - - // The 'file' backend is always enabled. - Backends: map[string]yaml.MapSlice{ - "file": nil, - }, - } - if err := loadYAML(path, &config); err != nil { - return nil, err - } - - // Load backend definitions from BackendsDir. - if config.BackendsDir != "" { - forAllFiles(filepath.Join(config.BackendsDir, "*.yml"), func(f string) { - tmp, ferr := loadStandaloneBackendConfig(f) - if ferr != nil { - log.Printf("configuration error: %s: %v", f, ferr) - return - } - for name, b := range tmp { - config.Backends[name] = b - } - }) - } - - // Load service definitions from a directory if necessary, and - // merge them into config.Services. - if config.ServicesDir != "" { - forAllFiles(filepath.Join(config.ServicesDir, "*.yml"), func(f string) { - tmp, ferr := loadStandaloneServiceConfig(f) - if ferr != nil { - log.Printf("configuration error: %s: %v", f, ferr) - return - } - for name, svc := range tmp { - config.Services[name] = svc - } - }) - } - - return &config, nil -} - // Server is the main authentication server object. type Server struct { services map[string]*service - backends map[string]UserBackend + backends map[string]userBackend u2fShortTerm U2FShortTermStorage otpShortTerm OTPShortTermStorage } @@ -650,15 +517,6 @@ func (s *Server) checkOTP(user *User, otp, secret string) bool { return totp.Validate(otp, secret) } -// Unmarshal a partially-parsed yaml.MapSlice. -func unmarshalMapSlice(raw yaml.MapSlice, obj interface{}) error { - b, err := yaml.Marshal(raw) - if err != nil { - return err - } - return yaml.Unmarshal(b, obj) -} - func newError() *auth.Response { return &auth.Response{Status: auth.StatusError} } diff --git a/server/config.go b/server/config.go new file mode 100644 index 0000000000000000000000000000000000000000..87eb28023e4bdd3f2ac67b28e2298fa6d8d33ee3 --- /dev/null +++ b/server/config.go @@ -0,0 +1,148 @@ +package server + +import ( + "io/ioutil" + "log" + "path/filepath" + + "git.autistici.org/ai3/go-common/clientutil" + "gopkg.in/yaml.v2" +) + +// BackendSpec specifies backend-specific configuration for a service. +type BackendSpec struct { + BackendName string `yaml:"backend"` + Params yaml.MapSlice `yaml:"params"` + StaticGroups []string `yaml:"static_groups"` +} + +// ServiceConfig defines the authentication backends for a service. +type ServiceConfig struct { + BackendSpecs []*BackendSpec `yaml:"backends"` + ChallengeResponse bool `yaml:"challenge_response"` + Enforce2FA bool `yaml:"enforce_2fa"` + EnableDeviceTracking bool `yaml:"enable_device_tracking"` + Ratelimits []string `yaml:"rate_limits"` +} + +// Config for the authentication server. +type Config struct { + // Service-specific configuration. + Services map[string]*ServiceConfig `yaml:"services"` + + // If set, load more service definitions from *.yml files in this directory. + ServicesDir string `yaml:"services_dir"` + + // Enabled backends. + Backends map[string]yaml.MapSlice `yaml:"enabled_backends"` + + // If set, load more backend definitions from *.yml files in this directory. + BackendsDir string `yaml:"backends_dir"` + + // Named rate limiter configurations. + RateLimiters map[string]*authRatelimiterConfig `yaml:"rate_limits"` + + // Configuration for the user-meta-server backend. + UserMetaDBConfig *clientutil.BackendConfig `yaml:"user_meta_server"` + + // Memcache servers used for short-term storage. + MemcacheServers []string `yaml:"memcache_servers"` + + // Full path to the configuration file, used to resolve relative paths. + path string +} + +// Load and unmarshal a YAML file. +func loadYAML(path string, obj interface{}) error { + data, err := ioutil.ReadFile(path) // #nosec + if err != nil { + return err + } + return yaml.Unmarshal(data, obj) +} + +// Load a standalone service configuration: a YAML-encoded file that +// may contain one or more ServiceConfig definitions. +func loadStandaloneServiceConfig(path string) (map[string]*ServiceConfig, error) { + var out map[string]*ServiceConfig + if err := loadYAML(path, &out); err != nil { + return nil, err + } + return out, nil +} + +// Load a standalone service configuration: a YAML-encoded file that +// may contain one or more ServiceConfig definitions. +func loadStandaloneBackendConfig(path string) (map[string]yaml.MapSlice, error) { + var out map[string]yaml.MapSlice + if err := loadYAML(path, &out); err != nil { + return nil, err + } + return out, nil +} + +// Apply a function to all files matching a glob pattern (errors are ignored). +func forAllFiles(pattern string, f func(string)) { + files, err := filepath.Glob(pattern) + if err != nil { + return + } + for _, file := range files { + f(file) + } +} + +// LoadConfig loads the configuration from a YAML-encoded file. +func LoadConfig(path string) (*Config, error) { + config := Config{ + path: path, + + // The 'file' backend is always enabled. + Backends: map[string]yaml.MapSlice{ + "file": nil, + }, + } + if err := loadYAML(path, &config); err != nil { + return nil, err + } + + // Load backend definitions from BackendsDir. + if config.BackendsDir != "" { + forAllFiles(filepath.Join(config.BackendsDir, "*.yml"), func(f string) { + tmp, ferr := loadStandaloneBackendConfig(f) + if ferr != nil { + log.Printf("configuration error: %s: %v", f, ferr) + return + } + for name, b := range tmp { + config.Backends[name] = b + } + }) + } + + // Load service definitions from a directory if necessary, and + // merge them into config.Services. + if config.ServicesDir != "" { + forAllFiles(filepath.Join(config.ServicesDir, "*.yml"), func(f string) { + tmp, ferr := loadStandaloneServiceConfig(f) + if ferr != nil { + log.Printf("configuration error: %s: %v", f, ferr) + return + } + for name, svc := range tmp { + config.Services[name] = svc + } + }) + } + + return &config, nil +} + +// Unmarshal a partially-parsed yaml.MapSlice. +func unmarshalMapSlice(raw yaml.MapSlice, obj interface{}) error { + b, err := yaml.Marshal(raw) + if err != nil { + return err + } + return yaml.Unmarshal(b, obj) +}