Files
opencloud/vendor/github.com/libregraph/idm/server/server.go
2023-04-19 20:24:34 +02:00

255 lines
6.4 KiB
Go

/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2021 The LibreGraph Authors.
*/
package server
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/bombsimon/logrusr/v3"
"github.com/go-ldap/ldap/v3"
"github.com/sirupsen/logrus"
"github.com/libregraph/idm/pkg/ldapserver"
"github.com/libregraph/idm/server/handler"
"github.com/libregraph/idm/server/handler/boltdb"
"github.com/libregraph/idm/server/handler/ldif"
)
const DefaultGeneratedPasswordLength = 16
// Server is our server implementation.
type Server struct {
config *Config
logger logrus.FieldLogger
LDAPServer *ldapserver.Server
LDAPHandler handler.Handler
}
// NewServer constructs a server from the provided parameters.
func NewServer(c *Config) (*Server, error) {
s := &Server{
config: c,
logger: c.Logger,
}
s.LDAPServer = ldapserver.NewServer()
s.LDAPServer.EnforceLDAP = false
s.LDAPServer.GeneratedPasswordLength = DefaultGeneratedPasswordLength
ldapserver.Logger(logrusr.New(c.Logger))
var err error
switch c.LDAPHandler {
case "ldif":
ldifHandlerOptions := &ldif.Options{
BaseDN: s.config.LDAPBaseDN,
AdminDN: s.config.LDAPAdminDN,
AllowLocalAnonymousBind: s.config.LDAPAllowLocalAnonymousBind,
DefaultCompany: s.config.LDIFDefaultCompany,
DefaultMailDomain: s.config.LDIFDefaultMailDomain,
TemplateExtraVars: s.config.LDIFTemplateExtraVars,
TemplateDebug: os.Getenv("KIDM_TEMPLATE_DEBUG") != "",
}
s.LDAPHandler, err = ldif.NewLDIFHandler(s.logger, s.config.LDIFMain, ldifHandlerOptions)
if err != nil {
return nil, fmt.Errorf("failed to create LDIF source handler: %w", err)
}
if s.config.LDIFConfig != "" {
middleware, middlewareErr := ldif.NewLDIFMiddleware(s.logger, s.config.LDIFConfig, ldifHandlerOptions)
if middlewareErr != nil {
return nil, fmt.Errorf("failed to create LDIF config handler: %w", middlewareErr)
}
s.LDAPHandler = middleware.WithHandler(s.LDAPHandler)
}
case "boltdb":
boltOptions := &boltdb.Options{
BaseDN: s.config.LDAPBaseDN,
AdminDN: s.config.LDAPAdminDN,
AllowLocalAnonymousBind: s.config.LDAPAllowLocalAnonymousBind,
}
s.LDAPHandler, err = boltdb.NewBoltDBHandler(s.logger, s.config.BoltDBFile, boltOptions)
if err != nil {
return nil, fmt.Errorf("failed to create BoltDB handler: %w", err)
}
// FIXME Let the frontend (LDAPServer) handle filtering and attribute list until we added backend support
s.LDAPServer.EnforceLDAP = true
default:
return nil, fmt.Errorf("unknown LDAPHandler: '%s'", c.LDAPHandler)
}
if c.Metrics != nil {
s.LDAPServer.SetStats(true)
MustRegister(c.Metrics, NewLDAPServerCollector(s.LDAPServer))
}
return s, nil
}
// Serve starts all the accociated servers resources and listeners and blocks
// forever until signals or error occurs.
func (s *Server) Serve(ctx context.Context) error {
var err error
serveCtx, serveCtxCancel := context.WithCancel(ctx)
defer serveCtxCancel()
logger := s.logger
errCh := make(chan error, 2)
exitCh := make(chan struct{}, 1)
signalCh := make(chan os.Signal, 1)
readyCh := make(chan struct{}, 1)
triggerCh := make(chan bool, 1)
go func() {
select {
case <-serveCtx.Done():
return
case <-readyCh:
}
logger.WithFields(logrus.Fields{}).Infoln("ready")
}()
var serversWg sync.WaitGroup
// NOTE(rhafer): since v3.4.3 the ldap package allows to set a custom logger.
// Set that to use to our logger.
loggerWriter := logger.WithField("scope", "ldap").WriterLevel(logrus.DebugLevel)
defer loggerWriter.Close()
ldap.Logger(log.New(loggerWriter, "", 0))
ldapHandler := s.LDAPHandler.WithContext(serveCtx)
s.LDAPServer.AddFunc("", ldapHandler)
s.LDAPServer.BindFunc("", ldapHandler)
s.LDAPServer.DeleteFunc("", ldapHandler)
s.LDAPServer.ModifyFunc("", ldapHandler)
s.LDAPServer.ModifyDNFunc("", ldapHandler)
s.LDAPServer.PasswordExOpFunc("", ldapHandler)
s.LDAPServer.SearchFunc("", ldapHandler)
s.LDAPServer.CloseFunc("", ldapHandler)
serversWg.Add(1)
go func() {
defer serversWg.Done()
for {
select {
case <-triggerCh:
reloadErr := ldapHandler.Reload(serveCtx)
if reloadErr != nil {
logger.Debugln("reload error: %w", reloadErr)
} else {
logger.Debugln("reload complete")
}
case <-serveCtx.Done():
return
}
}
}()
if s.config.LDAPListenAddr != "" {
serversWg.Add(1)
go func() {
defer serversWg.Done()
logger.WithField("listen_addr", s.config.LDAPListenAddr).Infoln("starting LDAP listener")
serveErr := s.LDAPServer.ListenAndServe(s.config.LDAPListenAddr)
if serveErr != nil {
errCh <- serveErr
}
}()
}
if s.config.LDAPSListenAddr != "" {
serversWg.Add(1)
go func() {
defer serversWg.Done()
logger.WithField("listen_addr_tls", s.config.LDAPSListenAddr).Infoln("starting LDAPS listener")
serveErr := s.LDAPServer.ListenAndServeTLS(s.config.LDAPSListenAddr, s.config.TLSCertFile, s.config.TLSKeyFile)
if serveErr != nil {
errCh <- serveErr
}
}()
}
go func() {
serversWg.Wait()
logger.Debugln("server listeners stopped")
close(exitCh)
}()
go func() {
close(readyCh) // TODO(longsleep): Implement real ready.
if s.config.OnReady != nil {
go s.config.OnReady(s)
}
}()
// Wait for exit or error, with support for HUP to reload
err = func() error {
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
for {
select {
case errFromChannel := <-errCh:
return errFromChannel
case reason := <-signalCh:
if reason == syscall.SIGHUP {
logger.Infoln("reload signal received")
select {
case triggerCh <- true:
default:
}
continue
}
logger.WithField("signal", reason).Warnln("received signal")
return nil
}
}
}()
// Shutdown, server will stop to accept new connections, requires Go 1.8+.
logger.Infoln("clean server shutdown start")
_, shutdownCtxCancel := context.WithTimeout(ctx, 10*time.Second)
go func() {
close(s.LDAPServer.Quit)
}()
// Cancel our own context,
serveCtxCancel()
func() {
for {
select {
case <-exitCh:
logger.Infoln("clean server shutdown complete, exiting")
return
default:
// Services have not quit yet.
logger.Info("waiting for services to exit")
}
select {
case reason := <-signalCh:
logger.WithField("signal", reason).Warn("received signal")
return
case <-time.After(100 * time.Millisecond):
}
}
}()
shutdownCtxCancel() // Prevents leak.
return err
}