[server][auth] ldap backend

This commit is contained in:
Abhishek Shroff
2025-06-26 09:14:40 +05:30
parent 9c4ca66cb9
commit c80ca7a3fb
4 changed files with 207 additions and 0 deletions
+3
View File
@@ -10,6 +10,7 @@ require (
github.com/gin-contrib/cors v1.5.0
github.com/gin-contrib/static v1.1.5
github.com/gin-gonic/gin v1.10.0
github.com/go-ldap/ldap/v3 v3.4.11
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.5
github.com/jackc/tern/v2 v2.2.1
@@ -28,6 +29,8 @@ require (
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
+22
View File
@@ -1,3 +1,5 @@
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -7,6 +9,8 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
@@ -40,6 +44,10 @@ github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmn
github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -61,6 +69,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
@@ -79,6 +89,18 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/tern/v2 v2.2.1 h1:kricKrvA6FNzBHHaQu15hmJDnpHvZA2DoJa97lJLt10=
github.com/jackc/tern/v2 v2.2.1/go.mod h1:thNyC7gVBGYWsAJJSvAX0ML/1lAmOw7+DVH8aSE5rto=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
+169
View File
@@ -0,0 +1,169 @@
package ldap
import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/url"
"strings"
"sync"
"time"
"github.com/go-ldap/ldap/v3"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
type Config struct {
Debug bool `koanf:"debug"`
URL string `koanf:"url"`
StartTLS bool `koanf:"starttls"`
ConnectTimeout string `koanf:"connect_timeout"`
RequestTimeout string `koanf:"request_timeout"`
DNTemplate string `koanf:"dn_template"`
Search SearchConfig `koanf:"search"`
}
type SearchConfig struct {
BindDN string `koanf:"bind_dn"`
BindPassword string `koanf:"bind_password"`
BaseDN string `koanf:"base_dn"`
FilterTemplate string `koanf:"filter_template"`
}
type Auth struct {
log zerolog.Logger
url string
hostName string
startTLS bool
conn *ldap.Conn
connLock sync.Mutex
connectTimeout time.Duration
readTimeout time.Duration
dnTemplate string
search SearchConfig
}
var a Auth
func Init(cfg Config, log zerolog.Logger) error {
a.log = log.With().Str("c", "ldap").Logger()
if cfg.Debug {
a.log = log.Level(zerolog.DebugLevel)
}
info := a.log.Debug()
if cfg.DNTemplate == "" {
if cfg.Search.BaseDN == "" {
return errors.New("base_dn not set")
}
if cfg.Search.FilterTemplate == "" {
return errors.New("filter_template not set")
}
info = info.Str("base_dn", cfg.Search.BaseDN)
info = info.Str("filter_template", cfg.DNTemplate)
} else {
info = info.Str("dn_template", cfg.DNTemplate)
}
info.Msg("Using ldap auth")
if parsedURL, err := url.Parse(cfg.URL); err != nil {
return errors.New("invalid server URL: " + err.Error())
} else {
a.url = cfg.URL
a.startTLS = cfg.StartTLS
a.hostName = strings.Split(parsedURL.Host, ":")[0]
}
if cfg.ConnectTimeout == "" {
a.connectTimeout = 60 * time.Second
} else if d, err := time.ParseDuration(cfg.ConnectTimeout); err != nil {
return errors.New("failed to parse connect timeout: " + err.Error())
} else {
a.connectTimeout = d
}
if cfg.RequestTimeout == "" {
a.readTimeout = 60 * time.Second
} else if d, err := time.ParseDuration(cfg.RequestTimeout); err != nil {
return errors.New("failed to parse read timeout: " + err.Error())
} else {
a.readTimeout = d
}
a.dnTemplate = cfg.DNTemplate
a.search = cfg.Search
var err error
a.conn, err = a.newConn()
if err != nil {
return errors.New("failed to connect: " + err.Error())
}
return nil
}
func (a *Auth) newConn() (*ldap.Conn, error) {
tlsCfg := &tls.Config{ServerName: a.hostName}
dialer := &net.Dialer{Timeout: a.connectTimeout}
conn, err := ldap.DialURL(a.url, ldap.DialWithDialer(dialer), ldap.DialWithTLSConfig(tlsCfg))
if err != nil {
log.Error().Err(err).Str("url", a.url).Msg("cannot contact directory server")
}
if a.readTimeout != 0 {
conn.SetTimeout(a.readTimeout)
}
if a.startTLS {
if err := conn.StartTLS(tlsCfg); err != nil {
return nil, fmt.Errorf("auth.ldap: %w", err)
}
}
return conn, nil
}
func (a *Auth) VerifyNew(email, password string) (bool, error) {
a.connLock.Lock()
defer a.connLock.Unlock()
var userDN string
if a.dnTemplate != "" {
userDN = strings.ReplaceAll(a.dnTemplate, "{email}", email)
} else {
if b, err := a.conn.SimpleBind(&ldap.SimpleBindRequest{
Username: a.search.BindDN,
Password: a.search.BindPassword,
AllowEmptyPassword: true,
}); err != nil {
return false, err
} else {
fmt.Printf("%+v", b)
}
req := ldap.NewSearchRequest(
a.search.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
2, 0, false,
strings.ReplaceAll(a.search.FilterTemplate, "{email}", email),
[]string{"dn"}, nil)
res, err := a.conn.Search(req)
if err != nil {
return false, errors.New("failed to search ldap: " + err.Error())
}
if len(res.Entries) > 1 {
return false, errors.New("too manu entries returned")
}
if len(res.Entries) == 0 {
return false, nil
}
userDN = res.Entries[0].DN
}
if err := a.conn.Bind(userDN, password); err != nil {
return false, err
}
return true, nil
}
@@ -33,6 +33,19 @@ auth:
upper: 1
numeric: 1
symbols: 1
ldap:
debug: false
url: ldap://ldap.example.com:1389
starttls: false
connect_timeout: 30s
request_timeout: 30s
dn_template: cn={email},ou=people,dc=example,dc=com
search:
bind_dn: cn=phylum,ou=people,dc=example,dc=com
bind_password:
base_dn: dc=example,dc=com
filter_template: (&(objectclass=person)(mail={email}))
jobs:
workers: 5