mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-05-12 15:18:38 -05:00
[server][auth] ldap backend
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user