Skip to content
4 changes: 3 additions & 1 deletion cmd/data-aggregation-api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
59 changes: 59 additions & 0 deletions internal/api/auth/basic_auth.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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 {
Expand Down
21 changes: 11 additions & 10 deletions internal/api/router/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package router

import (
"context"
"crypto/tls"
"fmt"
"net/http"

Expand Down Expand Up @@ -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)
Expand All @@ -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
}
32 changes: 19 additions & 13 deletions internal/config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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", "")
Expand Down
13 changes: 7 additions & 6 deletions settings.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ API:
ListenAddress: "127.0.0.1"
ListenPort: 1234

LDAP:
InsecureSkipVerify: "false"
URL: "ldaps://URL.local"
BindDN: "cn=<user>,OU=<ou>,DC=<local>"
BaseDN: "DC=<local>"
Password: "<some_password>"
Authentication:
LDAP:
InsecureSkipVerify: "false"
URL: "ldaps://URL.local"
BindDN: "cn=<user>,OU=<ou>,DC=<local>"
BaseDN: "DC=<local>"
Password: "<some_password>"

NetBox:
URL: "https://netbox.local"
Expand Down