diff --git a/cmd/data-aggregation-api/main.go b/cmd/data-aggregation-api/main.go index fc2989b..27cb2c7 100644 --- a/cmd/data-aggregation-api/main.go +++ b/cmd/data-aggregation-api/main.go @@ -56,7 +56,9 @@ func run() error { // TODO: be able to close goroutine when the context is closed (graceful shutdown) go job.StartBuildLoop(&deviceRepo, &reports) - router.NewManager(&deviceRepo, &reports).ListenAndServe(ctx, config.Cfg.API.ListenAddress, config.Cfg.API.ListenPort) + if err := router.NewManager(&deviceRepo, &reports).ListenAndServe(ctx, config.Cfg.API.ListenAddress, config.Cfg.API.ListenPort); err != nil { + return fmt.Errorf("webserver error: %w", err) + } return nil } diff --git a/internal/api/auth/basic_auth.go b/internal/api/auth/basic_auth.go index 555f354..f83d995 100644 --- a/internal/api/auth/basic_auth.go +++ b/internal/api/auth/basic_auth.go @@ -1,9 +1,14 @@ package auth import ( + "crypto/tls" + "errors" + "fmt" "net/http" + "github.com/criteo/data-aggregation-api/internal/config" "github.com/julienschmidt/httprouter" + "github.com/rs/zerolog/log" ) const ( @@ -12,6 +17,60 @@ const ( realm = `Basic realm="restricted"` ) +type authMode string + +const ( + noAuth authMode = "None" + ldapMode authMode = "LDAP" +) + +type BasicAuth struct { + ldapAuth *LDAPAuth + mode authMode +} + +func NewBasicAuth(cfg config.AuthConfig) (BasicAuth, error) { + b := BasicAuth{mode: noAuth} + + if cfg.LDAP == nil { + return b, nil + } + + tlsConfig := &tls.Config{ + InsecureSkipVerify: cfg.LDAP.InsecureSkipVerify, //nolint:gosec // configurable on purpose + } + ldap := NewLDAPAuth(cfg.LDAP.URL, cfg.LDAP.BindDN, cfg.LDAP.Password, cfg.LDAP.BaseDN, tlsConfig) + if err := b.configureLdap(ldap); err != nil { + return b, fmt.Errorf("failed to configure the request authenticator: %w", err) + } + b.mode = ldapMode + + return b, nil +} + +func (b *BasicAuth) configureLdap(ldap *LDAPAuth) error { + if ldap == nil { + return errors.New("LDAP configuration is missing") + } + b.ldapAuth = ldap + + return nil +} + +func (b *BasicAuth) Wrap(next httprouter.Handle) httprouter.Handle { + switch b.mode { + case noAuth: + return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { next(w, r, ps) } + case ldapMode: + return BasicAuthLDAP(b.ldapAuth, next) + default: + return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + log.Error().Str("auth-method", string(b.mode)).Str("authentication issue", "bad server configuration").Send() + http.Error(w, "authentication issue: bad server configuration", http.StatusInternalServerError) + } + } +} + // BasicAuthLDAP is a middleware wrapping the target HTTP HandlerFunc. // It retrieves BasicAuth credentials and authenticate against LDAP. func BasicAuthLDAP(ldapAuth *LDAPAuth, next httprouter.Handle) httprouter.Handle { diff --git a/internal/api/router/manager.go b/internal/api/router/manager.go index d085c9f..3aacd9d 100644 --- a/internal/api/router/manager.go +++ b/internal/api/router/manager.go @@ -2,7 +2,6 @@ package router import ( "context" - "crypto/tls" "fmt" "net/http" @@ -34,24 +33,24 @@ func NewManager(deviceRepo DevicesRepository, reports *report.Repository) *Manag } // ListenAndServe starts to serve Web API requests. -func (m *Manager) ListenAndServe(ctx context.Context, address string, port int) { +func (m *Manager) ListenAndServe(ctx context.Context, address string, port int) error { defer func() { log.Warn().Msg("Shutdown.") }() - tlsConfig := &tls.Config{ - InsecureSkipVerify: config.Cfg.LDAP.InsecureSkipVerify, //nolint:gosec // configurable on purpose + withAuth, err := auth.NewBasicAuth(config.Cfg.Authentication) + if err != nil { + return err } - ldap := auth.NewLDAPAuth(config.Cfg.LDAP.URL, config.Cfg.LDAP.BindDN, config.Cfg.LDAP.Password, config.Cfg.LDAP.BaseDN, tlsConfig) router := httprouter.New() router.GET("/api/health", healthCheck) - router.GET("/v1/devices/:hostname/afk_enabled", auth.BasicAuthLDAP(ldap, m.getAFKEnabled)) - router.GET("/v1/devices/:hostname/openconfig", auth.BasicAuthLDAP(ldap, m.getDeviceOpenConfig)) - router.GET("/v1/report/last", auth.BasicAuthLDAP(ldap, m.getLastReport)) - router.GET("/v1/report/last/complete", auth.BasicAuthLDAP(ldap, m.getLastCompleteReport)) - router.GET("/v1/report/last/successful", auth.BasicAuthLDAP(ldap, m.getLastSuccessfulReport)) + router.GET("/v1/devices/:hostname/afk_enabled", withAuth.Wrap(m.getAFKEnabled)) + router.GET("/v1/devices/:hostname/openconfig", withAuth.Wrap(m.getDeviceOpenConfig)) + router.GET("/v1/report/last", withAuth.Wrap(m.getLastReport)) + router.GET("/v1/report/last/complete", withAuth.Wrap(m.getLastCompleteReport)) + router.GET("/v1/report/last/successful", withAuth.Wrap(m.getLastSuccessfulReport)) listenSocket := fmt.Sprint(address, ":", port) log.Info().Msgf("Start webserver - listening on %s", listenSocket) @@ -69,4 +68,6 @@ func (m *Manager) ListenAndServe(ctx context.Context, address string, port int) if err := httpServer.Shutdown(context.Background()); err != nil { log.Error().Err(err).Send() } + + return nil } diff --git a/internal/config/settings.go b/internal/config/settings.go index 3c07081..4875c00 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -19,30 +19,35 @@ var ( ) type Config struct { + Authentication AuthConfig + NetBox struct { + URL string + APIKey string + } LogLevel string Datacenter string - - API struct { + API struct { ListenAddress string ListenPort int } - NetBox struct { - URL string - APIKey string - } - LDAP struct { - URL string - BaseDN string - BindDN string - Password string - InsecureSkipVerify bool - } Build struct { Interval time.Duration AllDevicesMustBuild bool } } +type AuthConfig struct { + LDAP *LDAPConfig +} + +type LDAPConfig struct { + URL string + BaseDN string + BindDN string + Password string + InsecureSkipVerify bool +} + func setDefaults() { viper.SetDefault("Datacenter", "") viper.SetDefault("LogLevel", "info") @@ -56,6 +61,7 @@ func setDefaults() { viper.SetDefault("Build.Interval", time.Minute) viper.SetDefault("Build.AllDevicesMustBuild", false) + viper.SetDefault("LDAP.Enabled", false) viper.SetDefault("LDAP.URL", "") viper.SetDefault("LDAP.BaseDN", "") viper.SetDefault("LDAP.BindDN", "") diff --git a/settings.example.yaml b/settings.example.yaml index 3767bfb..dc77092 100644 --- a/settings.example.yaml +++ b/settings.example.yaml @@ -5,12 +5,13 @@ API: ListenAddress: "127.0.0.1" ListenPort: 1234 -LDAP: - InsecureSkipVerify: "false" - URL: "ldaps://URL.local" - BindDN: "cn=,OU=,DC=" - BaseDN: "DC=" - Password: "" +Authentication: + LDAP: + InsecureSkipVerify: "false" + URL: "ldaps://URL.local" + BindDN: "cn=,OU=,DC=" + BaseDN: "DC=" + Password: "" NetBox: URL: "https://netbox.local"