mirror of
https://github.com/glauth/glauth.git
synced 2026-10-25 05:30:47 +00:00
Database plugins (#133)
This commit actually covers a few items. In future commits, I will keep features distinct. This is only happening this time around due to how long it took to merge this branch. Covered: - Database plugins (at this time: SQLite, MySQL, Postgres) - Backends acting as middleware: added the [[Backends]] configuration directive while retaining backward compatibility with [Backend] - Schema introspection (root DSE query with base scope) - When proxying, insert queried attribute back in upstream response, if absent, so that the LDAP library does not filter out all entries - When chaining backends, any backend can be used to inject OTP value in password, before reaching a non-OTP-aware backend (currently guarded by a True statement in case we find an issue (I did thoroughly test but you never know)) - Handling of special "1.1" attributes filter meaning "I do not want attributes" (RFC 4511, 4.5.1.8) - Support for "want types only" queries, even when proxying
This commit is contained in:
committed by
GitHub
parent
225f1b441b
commit
2eb2215ae4
@@ -18,6 +18,9 @@ BUILD_VARS=-s -w -X main.GitCommit=${GIT_COMMIT} -X main.GitBranch=${GIT_BRANCH}
|
||||
BUILD_FILES=glauth.go
|
||||
TRIM_FLAGS=-gcflags "all=-trimpath=${PWD}" -asmflags "all=-trimpath=${PWD}"
|
||||
|
||||
# Plugins
|
||||
include pkg/plugins/Makefile
|
||||
|
||||
#####################
|
||||
# High level commands
|
||||
#####################
|
||||
@@ -68,7 +71,6 @@ format:
|
||||
devrun:
|
||||
go run ${BUILD_FILES} -c sample-simple.cfg
|
||||
|
||||
|
||||
linux32:
|
||||
GOOS=linux GOARCH=386 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o bin/glauth32 ${BUILD_FILES} && cd bin && sha256sum glauth32 > glauth32.sha256
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/GeertJohan/yubigo"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
docopt "github.com/docopt/docopt-go"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/glauth/glauth/pkg/config"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
logging "github.com/op/go-logging"
|
||||
"gopkg.in/amz.v3/aws"
|
||||
"gopkg.in/amz.v3/s3"
|
||||
// "github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// Set with buildtime vars
|
||||
@@ -234,9 +236,6 @@ func parseConfigFile(configFileLocation string) (*config.Config, error) {
|
||||
// setup defaults
|
||||
cfg.LDAP.Enabled = false
|
||||
cfg.LDAPS.Enabled = true
|
||||
cfg.Backend.NameFormat = "cn"
|
||||
cfg.Backend.GroupFormat = "ou"
|
||||
cfg.Backend.SSHKeyAttr = "sshPublicKey"
|
||||
|
||||
// parse the config file
|
||||
if strings.HasPrefix(configFileLocation, "s3://") {
|
||||
@@ -277,6 +276,30 @@ func parseConfigFile(configFileLocation string) (*config.Config, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Backward Compability
|
||||
if cfg.Backend.Datastore != "" {
|
||||
if cfg.Backends != nil {
|
||||
return &cfg, fmt.Errorf("You cannot specify both [Backend] and [[Backends]] directives in the same configuration ")
|
||||
} else {
|
||||
cfg.Backends = append(cfg.Backends, cfg.Backend)
|
||||
}
|
||||
}
|
||||
spew.Dump(cfg.Backends)
|
||||
|
||||
// Patch with default values where not specified
|
||||
for i := range cfg.Backends {
|
||||
if cfg.Backends[i].NameFormat == "" {
|
||||
cfg.Backends[i].NameFormat = "cn"
|
||||
}
|
||||
if cfg.Backends[i].GroupFormat == "" {
|
||||
cfg.Backends[i].GroupFormat = "ou"
|
||||
}
|
||||
if cfg.Backends[i].SSHKeyAttr == "" {
|
||||
cfg.Backends[i].SSHKeyAttr = "sshPublicKey"
|
||||
}
|
||||
}
|
||||
//
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
@@ -354,14 +377,18 @@ func validateConfig(cfg config.Config) (*config.Config, error) {
|
||||
}
|
||||
}
|
||||
|
||||
switch cfg.Backend.Datastore {
|
||||
case "":
|
||||
cfg.Backend.Datastore = "config"
|
||||
case "config":
|
||||
case "ldap":
|
||||
case "owncloud":
|
||||
default:
|
||||
return &cfg, fmt.Errorf("invalid backend %s - must be 'config', 'ldap' or 'owncloud", cfg.Backend.Datastore)
|
||||
//spew.Dump(cfg)
|
||||
for i := range cfg.Backends {
|
||||
switch cfg.Backends[i].Datastore {
|
||||
case "":
|
||||
cfg.Backends[i].Datastore = "config"
|
||||
case "config":
|
||||
case "ldap":
|
||||
case "owncloud":
|
||||
case "plugin":
|
||||
default:
|
||||
return &cfg, fmt.Errorf("invalid backend %s - must be 'config', 'ldap', 'owncloud' or 'plugin'", cfg.Backends[i].Datastore)
|
||||
}
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
@@ -5,13 +5,19 @@ go 1.15
|
||||
require (
|
||||
github.com/GeertJohan/yubigo v0.0.0-20190917122436-175bc097e60e
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/go-logr/logr v0.4.0
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/hydronica/toml v0.4.2
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
|
||||
github.com/jteeuwen/go-bindata v3.0.7+incompatible // indirect
|
||||
github.com/kr/pretty v0.2.1
|
||||
github.com/lib/pq v1.10.0
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 // indirect
|
||||
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3
|
||||
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3 // indirect
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pquerna/otp v1.3.0
|
||||
github.com/yaegashi/msgraph.go v0.1.1-0.20200221123608-2d438cf2a7cc
|
||||
|
||||
@@ -7,23 +7,33 @@ github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyX
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc=
|
||||
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hydronica/toml v0.4.2 h1:BY+iAvyl2u1BlfgkJQzqc1UXDGLWfpMmOWuqJrwAVDU=
|
||||
github.com/hydronica/toml v0.4.2/go.mod h1:c7QhbYq3Wp9SlOWuG7MAieKUyXP2P/hXhy/YqWfbS/4=
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o=
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
|
||||
github.com/jteeuwen/go-bindata v3.0.7+incompatible h1:91Uy4d9SYVr1kyTJ15wJsog+esAZZl7JmEfTkwmhJts=
|
||||
github.com/jteeuwen/go-bindata v3.0.7+incompatible/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
|
||||
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 h1:D9EvfGQvlkKaDr2CRKN++7HbSXbefUNDrPq60T+g24s=
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484/go.mod h1:O1EljZ+oHprtxDDPHiMWVo/5dBT6PlvWX5PSwj80aBA=
|
||||
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3 h1:NNis9uuNpG5h97Dvxxo53Scg02qBg+3Nfabg6zjFGu8=
|
||||
|
||||
+21
-8
@@ -2,14 +2,25 @@ package config
|
||||
|
||||
// config file
|
||||
type Backend struct {
|
||||
BaseDN string
|
||||
Datastore string
|
||||
Insecure bool // For LDAP and owncloud backend only
|
||||
Servers []string // For LDAP and owncloud backend only
|
||||
NameFormat string
|
||||
GroupFormat string
|
||||
SSHKeyAttr string
|
||||
UseGraphAPI bool // For ownCloud backend only
|
||||
BaseDN string
|
||||
Datastore string
|
||||
Insecure bool // For LDAP and owncloud backend only
|
||||
Servers []string // For LDAP and owncloud backend only
|
||||
NameFormat string
|
||||
GroupFormat string
|
||||
SSHKeyAttr string
|
||||
UseGraphAPI bool // For ownCloud backend only
|
||||
Plugin string // Path to plugin library, for plugin backend only
|
||||
PluginHandler string // Name of plugin's main handler function
|
||||
Database string // For MySQL backend only TODO REname to match plugin
|
||||
}
|
||||
type Helper struct {
|
||||
Enabled bool
|
||||
BaseDN string
|
||||
Datastore string
|
||||
Plugin string // Path to plugin library, for plugin backend only
|
||||
PluginHandler string // Name of plugin's main handler function
|
||||
Database string // For MySQL backend only TODO REname to match plugin
|
||||
}
|
||||
type Frontend struct {
|
||||
AllowedBaseDNs []string // For LDAP backend only
|
||||
@@ -61,6 +72,8 @@ type Group struct {
|
||||
type Config struct {
|
||||
API API
|
||||
Backend Backend
|
||||
Backends []Backend
|
||||
Helper Helper
|
||||
Debug bool
|
||||
WatchConfig bool
|
||||
YubikeyClientID string
|
||||
|
||||
+77
-21
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -16,10 +17,14 @@ import (
|
||||
"github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
var configattributematcher = regexp.MustCompile(`(?i)\((?P<attribute>[a-zA-Z0-9]+)\s*=\s*(?P<value>.*)\)`)
|
||||
|
||||
type configHandler struct {
|
||||
backend config.Backend
|
||||
log logr.Logger
|
||||
cfg *config.Config
|
||||
yubikeyAuth *yubigo.YubiAuth
|
||||
attmatcher *regexp.Regexp
|
||||
}
|
||||
|
||||
// NewConfigHandler creates a new config backed handler
|
||||
@@ -27,9 +32,11 @@ func NewConfigHandler(opts ...Option) Handler {
|
||||
options := newOptions(opts...)
|
||||
|
||||
handler := configHandler{
|
||||
backend: options.Backend,
|
||||
log: options.Logger,
|
||||
cfg: options.Config,
|
||||
yubikeyAuth: options.YubiAuth,
|
||||
attmatcher: configattributematcher,
|
||||
}
|
||||
return handler
|
||||
}
|
||||
@@ -37,15 +44,15 @@ func NewConfigHandler(opts ...Option) Handler {
|
||||
// Bind implements a bind request against the config file
|
||||
func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCode ldap.LDAPResultCode, err error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
baseDN := strings.ToLower("," + h.cfg.Backend.BaseDN)
|
||||
baseDN := strings.ToLower("," + h.backend.BaseDN)
|
||||
|
||||
h.log.V(6).Info("Bind request", "binddn", bindDN, "basedn", h.cfg.Backend.BaseDN, "src", conn.RemoteAddr())
|
||||
h.log.V(6).Info("Bind request", "binddn", bindDN, "basedn", h.backend.BaseDN, "src", conn.RemoteAddr())
|
||||
|
||||
stats.Frontend.Add("bind_reqs", 1)
|
||||
|
||||
// parse the bindDN - ensure that the bindDN ends with the BaseDN
|
||||
if !strings.HasSuffix(bindDN, baseDN) {
|
||||
h.log.V(2).Info("BindDN not part of our BaseDN", "binddn", bindDN, "basedn", h.cfg.Backend.BaseDN)
|
||||
h.log.V(2).Info("BindDN not part of our BaseDN", "binddn", bindDN, "basedn", h.backend.BaseDN)
|
||||
// h.log.Warning(fmt.Sprintf("Bind Error: BindDN %s not our BaseDN %s", bindDN, baseDN))
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
@@ -53,10 +60,10 @@ func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultC
|
||||
groupName := ""
|
||||
userName := ""
|
||||
if len(parts) == 1 {
|
||||
userName = strings.TrimPrefix(parts[0], h.cfg.Backend.NameFormat+"=")
|
||||
userName = strings.TrimPrefix(parts[0], h.backend.NameFormat+"=")
|
||||
} else if len(parts) == 2 {
|
||||
userName = strings.TrimPrefix(parts[0], h.cfg.Backend.NameFormat+"=")
|
||||
groupName = strings.TrimPrefix(parts[1], h.cfg.Backend.GroupFormat+"=")
|
||||
userName = strings.TrimPrefix(parts[0], h.backend.NameFormat+"=")
|
||||
groupName = strings.TrimPrefix(parts[1], h.backend.GroupFormat+"=")
|
||||
} else {
|
||||
h.log.V(2).Info("BindDN should have only one or two parts", "binddn", bindDN, "numparts", len(parts))
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
@@ -66,20 +73,20 @@ func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultC
|
||||
user := config.User{}
|
||||
found := false
|
||||
for _, u := range h.cfg.Users {
|
||||
if u.Name == userName {
|
||||
if strings.EqualFold(u.Name, userName) {
|
||||
found = true
|
||||
user = u
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
h.log.V(2).Info(fmt.Sprintf("User not found", "username", userName))
|
||||
h.log.V(2).Info("User not found", "username", userName)
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
// find the group
|
||||
group := config.Group{}
|
||||
found = false
|
||||
for _, g := range h.cfg.Groups {
|
||||
if g.Name == groupName {
|
||||
if strings.EqualFold(g.Name, groupName) {
|
||||
found = true
|
||||
group = g
|
||||
}
|
||||
@@ -171,20 +178,57 @@ func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultC
|
||||
// Search implements a search request against the config file
|
||||
func (h configHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
baseDN := strings.ToLower("," + h.cfg.Backend.BaseDN)
|
||||
baseDN := strings.ToLower("," + h.backend.BaseDN)
|
||||
searchBaseDN := strings.ToLower(searchReq.BaseDN)
|
||||
h.log.V(6).Info("Search request", "binddn", bindDN, "basedn", baseDN, "src", conn.RemoteAddr(), "filter", searchReq.Filter)
|
||||
h.log.V(6).Info("Search request", "binddn", bindDN, "basedn", baseDN, "searchbasedn", searchBaseDN, "src", conn.RemoteAddr(), "scope", searchReq.Scope, "filter", searchReq.Filter)
|
||||
stats.Frontend.Add("search_reqs", 1)
|
||||
|
||||
// validate the user is authenticated and has appropriate access
|
||||
if len(bindDN) < 1 {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", bindDN)
|
||||
}
|
||||
if !strings.HasSuffix(bindDN, baseDN) {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", bindDN, h.cfg.Backend.BaseDN)
|
||||
|
||||
// for now, we are going to be dirty and assume that if scope==0 and dn=='' we are investigating the schema
|
||||
if searchReq.Scope == 0 && searchBaseDN == "" {
|
||||
entries := []*ldap.Entry{}
|
||||
attrs := []*ldap.EntryAttribute{}
|
||||
// unfortunately, objectClass is not to be included so we will respect that
|
||||
// attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"*"}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "supportedSASLMechanisms", Values: []string{}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "supportedLDAPVersion", Values: []string{"3"}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "supportedControl", Values: []string{}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "supportedCapabilities", Values: []string{}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "subschemaSubentry", Values: []string{baseDN}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "serverName", Values: []string{"unknown"}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "namingContexts", Values: []string{baseDN}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "defaultNamingContext", Values: []string{baseDN}})
|
||||
// ah, but you see, this is not enough because if your query is for, say 'objectClass', then our LDAP
|
||||
// library will weed out this entry since it does *not* contain an objectclass attribute
|
||||
attbits := h.attmatcher.FindStringSubmatch(searchReq.Filter)
|
||||
if len(attbits) == 3 {
|
||||
foundattname := false
|
||||
for _, attr := range attrs {
|
||||
if strings.ToLower(attr.Name) == strings.ToLower(attbits[1]) {
|
||||
foundattname = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// the ugly hack: we are going to pretend that the requested attribute is in there
|
||||
if !foundattname {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: attbits[1], Values: []string{attbits[2]}})
|
||||
}
|
||||
}
|
||||
entries = append(entries, &ldap.Entry{DN: "", Attributes: attrs})
|
||||
stats.Frontend.Add("search_successes", 1)
|
||||
h.log.V(6).Info("AP: Root Search OK", "filter", searchReq.Filter)
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
if !strings.HasSuffix(searchBaseDN, h.cfg.Backend.BaseDN) {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: search BaseDN %s is not in our BaseDN %s", searchBaseDN, h.cfg.Backend.BaseDN)
|
||||
|
||||
if !strings.HasSuffix(bindDN, baseDN) {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", bindDN, h.backend.BaseDN)
|
||||
}
|
||||
if !strings.HasSuffix(searchBaseDN, h.backend.BaseDN) {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: search BaseDN %s is not in our BaseDN %s", searchBaseDN, h.backend.BaseDN)
|
||||
}
|
||||
// return all users in the config file - the LDAP library will filter results for us
|
||||
entries := []*ldap.Entry{}
|
||||
@@ -205,7 +249,7 @@ func (h configHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"posixGroup"}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "uniqueMember", Values: h.getGroupMembers(g.UnixID)})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberUid", Values: h.getGroupMemberIDs(g.UnixID)})
|
||||
dn := fmt.Sprintf("cn=%s,%s=groups,%s", g.Name, h.cfg.Backend.GroupFormat, h.cfg.Backend.BaseDN)
|
||||
dn := fmt.Sprintf("cn=%s,%s=groups,%s", g.Name, h.backend.GroupFormat, h.backend.BaseDN)
|
||||
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
|
||||
}
|
||||
case "posixaccount", "shadowaccount", "":
|
||||
@@ -263,9 +307,9 @@ func (h configHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowWarning", Values: []string{"7"}})
|
||||
|
||||
if len(u.SSHKeys) > 0 {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: h.cfg.Backend.SSHKeyAttr, Values: u.SSHKeys})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: h.backend.SSHKeyAttr, Values: u.SSHKeys})
|
||||
}
|
||||
dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.cfg.Backend.NameFormat, u.Name, h.cfg.Backend.GroupFormat, h.getGroupName(u.PrimaryGroup), h.cfg.Backend.BaseDN)
|
||||
dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(u.PrimaryGroup), h.backend.BaseDN)
|
||||
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
|
||||
}
|
||||
}
|
||||
@@ -289,6 +333,18 @@ func (h configHandler) Delete(boundDN string, deleteDN string, conn net.Conn) (r
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
func (h configHandler) FindUser(userName string) (f bool, u config.User, err error) {
|
||||
user := config.User{}
|
||||
found := false
|
||||
for _, u := range h.cfg.Users {
|
||||
if strings.EqualFold(u.Name, userName) {
|
||||
found = true
|
||||
user = u
|
||||
}
|
||||
}
|
||||
return found, user, nil
|
||||
}
|
||||
|
||||
// Close does not actually close anything, because the config data is kept in memory
|
||||
func (h configHandler) Close(boundDn string, conn net.Conn) error {
|
||||
stats.Frontend.Add("closes", 1)
|
||||
@@ -299,12 +355,12 @@ func (h configHandler) getGroupMembers(gid int) []string {
|
||||
members := make(map[string]bool)
|
||||
for _, u := range h.cfg.Users {
|
||||
if u.PrimaryGroup == gid {
|
||||
dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.cfg.Backend.NameFormat, u.Name, h.cfg.Backend.GroupFormat, h.getGroupName(u.PrimaryGroup), h.cfg.Backend.BaseDN)
|
||||
dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(u.PrimaryGroup), h.backend.BaseDN)
|
||||
members[dn] = true
|
||||
} else {
|
||||
for _, othergid := range u.OtherGroups {
|
||||
if othergid == gid {
|
||||
dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.cfg.Backend.NameFormat, u.Name, h.cfg.Backend.GroupFormat, h.getGroupName(u.PrimaryGroup), h.cfg.Backend.BaseDN)
|
||||
dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(u.PrimaryGroup), h.backend.BaseDN)
|
||||
members[dn] = true
|
||||
}
|
||||
}
|
||||
@@ -381,7 +437,7 @@ func (h configHandler) getGroupDNs(gids []int) []string {
|
||||
for _, gid := range gids {
|
||||
for _, g := range h.cfg.Groups {
|
||||
if g.UnixID == gid {
|
||||
dn := fmt.Sprintf("cn=%s,%s=groups,%s", g.Name, h.cfg.Backend.GroupFormat, h.cfg.Backend.BaseDN)
|
||||
dn := fmt.Sprintf("cn=%s,%s=groups,%s", g.Name, h.backend.GroupFormat, h.backend.BaseDN)
|
||||
groups[dn] = true
|
||||
}
|
||||
|
||||
|
||||
+19
-1
@@ -1,6 +1,13 @@
|
||||
package handler
|
||||
|
||||
import "github.com/nmcclain/ldap"
|
||||
import (
|
||||
"github.com/glauth/glauth/pkg/config"
|
||||
"github.com/nmcclain/ldap"
|
||||
)
|
||||
|
||||
type HelperMaker interface {
|
||||
FindUser(userName string) (bool, config.User, error)
|
||||
}
|
||||
|
||||
// Handler is the common interface for all datastores
|
||||
type Handler interface {
|
||||
@@ -13,4 +20,15 @@ type Handler interface {
|
||||
ldap.Adder
|
||||
ldap.Modifier // Note: modifying eg the uid or cn might change the dn because the hierarchy is determined by the backend
|
||||
ldap.Deleter
|
||||
|
||||
// helper
|
||||
HelperMaker
|
||||
}
|
||||
|
||||
// TODO When I grow up, I want to handle pointers same as I would in C
|
||||
// and not need a counter because I would not allocate statically
|
||||
// but use idiomatic slicing instead
|
||||
type HandlerWrapper struct {
|
||||
Handlers []Handler
|
||||
Count *int
|
||||
}
|
||||
|
||||
+138
-4
@@ -8,6 +8,7 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -17,15 +18,23 @@ import (
|
||||
"github.com/glauth/glauth/pkg/stats"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/nmcclain/ldap"
|
||||
"github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
// global matcher
|
||||
var ldapattributematcher = regexp.MustCompile(`(?i)\((?P<attribute>[a-zA-Z0-9]+)\s*=\s*(?P<value>.*)\)`)
|
||||
|
||||
type ldapHandler struct {
|
||||
backend config.Backend
|
||||
handlers HandlerWrapper
|
||||
doPing chan bool
|
||||
log logr.Logger
|
||||
cfg *config.Config
|
||||
lock *sync.Mutex // for sessions and servers
|
||||
sessions map[string]ldapSession
|
||||
servers []ldapBackend
|
||||
helper Handler
|
||||
attm *regexp.Regexp
|
||||
}
|
||||
|
||||
// global lock for ldapHandler sessions & servers manipulation
|
||||
@@ -55,19 +64,22 @@ func NewLdapHandler(opts ...Option) Handler {
|
||||
options := newOptions(opts...)
|
||||
|
||||
handler := ldapHandler{ // set non-zero-value defaults here
|
||||
backend: options.Backend,
|
||||
handlers: options.Handlers,
|
||||
sessions: make(map[string]ldapSession),
|
||||
doPing: make(chan bool),
|
||||
log: options.Logger,
|
||||
cfg: options.Config,
|
||||
helper: options.Helper,
|
||||
lock: &ldaplock,
|
||||
attm: ldapattributematcher,
|
||||
}
|
||||
// parse LDAP URLs
|
||||
for _, ldapurl := range handler.cfg.Backend.Servers {
|
||||
for _, ldapurl := range handler.backend.Servers {
|
||||
l, err := parseURL(ldapurl)
|
||||
if err != nil {
|
||||
handler.log.Error(err, "could not parse url")
|
||||
os.Exit(1)
|
||||
// TODO log error and os exit
|
||||
}
|
||||
handler.servers = append(handler.servers, l)
|
||||
}
|
||||
@@ -82,6 +94,51 @@ func NewLdapHandler(opts ...Option) Handler {
|
||||
func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCode ldap.LDAPResultCode, err error) {
|
||||
h.log.V(6).Info("Bind request", "binddn", bindDN, "src", conn.RemoteAddr())
|
||||
|
||||
// if h.helper != nil {
|
||||
if true {
|
||||
|
||||
lowerBindDN := strings.ToLower(bindDN)
|
||||
baseDN := strings.ToLower("," + h.backend.BaseDN)
|
||||
parts := strings.Split(strings.TrimSuffix(lowerBindDN, baseDN), ",")
|
||||
userName := strings.TrimPrefix(parts[0], h.backend.NameFormat+"=")
|
||||
|
||||
validotp := false
|
||||
|
||||
// Find the user
|
||||
// We are going to go through all backends and ask
|
||||
// until we find our user or die of boredom.
|
||||
user := config.User{}
|
||||
found := false
|
||||
for i, handler := range h.handlers.Handlers {
|
||||
found, user, _ = handler.FindUser(userName)
|
||||
if found {
|
||||
break
|
||||
}
|
||||
if i >= *h.handlers.Count {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
validotp = true
|
||||
} else {
|
||||
if len(user.OTPSecret) == 0 {
|
||||
validotp = true
|
||||
} else {
|
||||
if len(bindSimplePw) > 6 {
|
||||
otp := bindSimplePw[len(bindSimplePw)-6:]
|
||||
bindSimplePw = bindSimplePw[:len(bindSimplePw)-6]
|
||||
validotp = totp.Validate(otp, user.OTPSecret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !validotp {
|
||||
h.log.V(6).Info(fmt.Sprintf("Bind Error: invalid OTP token as %s from %s", bindDN, conn.RemoteAddr().String()))
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
}
|
||||
|
||||
stats.Frontend.Add("bind_reqs", 1)
|
||||
s, err := h.getSession(conn)
|
||||
if err != nil {
|
||||
@@ -101,7 +158,25 @@ func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCod
|
||||
|
||||
//
|
||||
func (h ldapHandler) Search(boundDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) {
|
||||
wantAttributes := true
|
||||
wantTypesOnly := false
|
||||
|
||||
h.log.V(6).Info("Search request", "binddn", boundDN, "src", conn.RemoteAddr(), "filter", searchReq.Filter)
|
||||
|
||||
// "1.1" has special meaning: it does what an empty attribute list would do
|
||||
// if it didn't already mean "return all attributes"
|
||||
if len(searchReq.Attributes) == 1 && searchReq.Attributes[0] == "1.1" {
|
||||
wantAttributes = false
|
||||
searchReq.Attributes = searchReq.Attributes[:0]
|
||||
}
|
||||
|
||||
// TypesOnly cannot be true: if it were, glauth would not be able to
|
||||
// match the returned valuea against the query
|
||||
if searchReq.TypesOnly == true {
|
||||
wantTypesOnly = true
|
||||
searchReq.TypesOnly = false
|
||||
}
|
||||
|
||||
stats.Frontend.Add("search_reqs", 1)
|
||||
s, err := h.getSession(conn)
|
||||
if err != nil {
|
||||
@@ -123,6 +198,61 @@ func (h ldapHandler) Search(boundDN string, searchReq ldap.SearchRequest, conn n
|
||||
h.log.V(6).Info("Search request to backend", "request", search)
|
||||
sr, err := s.ldap.Search(search)
|
||||
h.log.V(6).Info("Backend Search result", "result", sr)
|
||||
|
||||
if !wantAttributes {
|
||||
h.log.V(6).Info("AP: Search Info", "type", "No attributes")
|
||||
for _, entry := range sr.Entries {
|
||||
entry.Attributes = entry.Attributes[:0]
|
||||
}
|
||||
}
|
||||
|
||||
if wantTypesOnly {
|
||||
h.log.V(6).Info("AP: Search Info", "type", "Types only")
|
||||
for _, entry := range sr.Entries {
|
||||
for _, attribute := range entry.Attributes {
|
||||
attribute.Values = attribute.Values[:0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WART used to debug when testing special cases against
|
||||
// glauth acting as a backend, where it may have
|
||||
// the same workaround thus hiding the issue
|
||||
/*
|
||||
for _, entry := range sr.Entries {
|
||||
for _, attribute := range entry.Attributes {
|
||||
if attribute.Name == "objectclass" {
|
||||
attribute.Name = "bogus"
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// If our original attribute is not present, either because:
|
||||
// 1-This is a root query
|
||||
// 2-We were asked not to return attributes
|
||||
// 3-We were asked not to return values
|
||||
// then we re-insert the correct values in there.
|
||||
if searchReq.Scope == 0 && searchReq.BaseDN == "" {
|
||||
h.log.V(6).Info("AP: Search Info", "type", "Root search detected")
|
||||
}
|
||||
attbits := h.attm.FindStringSubmatch(searchReq.Filter)
|
||||
for _, entry := range sr.Entries {
|
||||
foundattname := false
|
||||
for _, attribute := range entry.Attributes {
|
||||
if strings.ToLower(attribute.Name) == strings.ToLower(attbits[1]) {
|
||||
foundattname = true
|
||||
if len(attbits[2]) == 0 {
|
||||
attribute.Values = []string{attbits[2]}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundattname {
|
||||
entry.Attributes = append(entry.Attributes, &ldap.EntryAttribute{Name: attbits[1], Values: []string{attbits[2]}})
|
||||
}
|
||||
}
|
||||
|
||||
ssr := ldap.ServerSearchResult{
|
||||
Entries: sr.Entries,
|
||||
Referrals: sr.Referrals,
|
||||
@@ -156,6 +286,10 @@ func (h ldapHandler) Delete(boundDN string, deleteDN string, conn net.Conn) (res
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
func (h ldapHandler) FindUser(userName string) (found bool, user config.User, err error) {
|
||||
return false, config.User{}, nil
|
||||
}
|
||||
|
||||
func (h ldapHandler) Close(boundDn string, conn net.Conn) error {
|
||||
conn.Close() // close connection to the server when then client is closed
|
||||
h.lock.Lock()
|
||||
@@ -213,7 +347,7 @@ func (h ldapHandler) getSession(conn net.Conn) (ldapSession, error) {
|
||||
dest := fmt.Sprintf("%s:%d", server.Hostname, server.Port)
|
||||
if server.Scheme == "ldaps" {
|
||||
tlsCfg := &tls.Config{}
|
||||
if h.cfg.Backend.Insecure {
|
||||
if h.backend.Insecure {
|
||||
tlsCfg.InsecureSkipVerify = true
|
||||
}
|
||||
l, err = ldap.DialTLS("tcp", dest, tlsCfg)
|
||||
@@ -245,7 +379,7 @@ func (h ldapHandler) ping() error {
|
||||
start := time.Now()
|
||||
if h.servers[0].Scheme == "ldaps" {
|
||||
tlsCfg := &tls.Config{}
|
||||
if h.cfg.Backend.Insecure {
|
||||
if h.backend.Insecure {
|
||||
tlsCfg.InsecureSkipVerify = true
|
||||
}
|
||||
l, err = ldap.DialTLS("tcp", dest, tlsCfg)
|
||||
|
||||
@@ -13,10 +13,13 @@ type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Backend config.Backend
|
||||
Handlers HandlerWrapper
|
||||
Logger logr.Logger
|
||||
Config *config.Config
|
||||
Context *context.Context
|
||||
YubiAuth *yubigo.YubiAuth
|
||||
Helper Handler
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
@@ -30,6 +33,31 @@ func newOptions(opts ...Option) Options {
|
||||
return opt
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func NewOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Backend is our current backend
|
||||
func Backend(val config.Backend) Option {
|
||||
return func(o *Options) {
|
||||
o.Backend = val
|
||||
}
|
||||
}
|
||||
|
||||
// Our friendly handlers for all backends
|
||||
func Handlers(val HandlerWrapper) Option {
|
||||
return func(o *Options) {
|
||||
o.Handlers = val
|
||||
}
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val logr.Logger) Option {
|
||||
return func(o *Options) {
|
||||
@@ -57,3 +85,10 @@ func YubiAuth(val *yubigo.YubiAuth) Option {
|
||||
o.YubiAuth = val
|
||||
}
|
||||
}
|
||||
|
||||
// If we specified a helper, for instance for OTP injection
|
||||
func Helper(val Handler) Option {
|
||||
return func(o *Options) {
|
||||
o.Helper = val
|
||||
}
|
||||
}
|
||||
|
||||
+24
-18
@@ -28,6 +28,7 @@ type ownCloudSession struct {
|
||||
useGraphAPI bool
|
||||
}
|
||||
type ownCloudHandler struct {
|
||||
backend config.Backend
|
||||
log logr.Logger
|
||||
cfg *config.Config
|
||||
client *http.Client
|
||||
@@ -37,15 +38,15 @@ type ownCloudHandler struct {
|
||||
|
||||
func (h ownCloudHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
baseDN := strings.ToLower("," + h.cfg.Backend.BaseDN)
|
||||
baseDN := strings.ToLower("," + h.backend.BaseDN)
|
||||
|
||||
h.log.V(6).Info("Bind request", "binddn", bindDN, "basedn", h.cfg.Backend.BaseDN, "src", conn.RemoteAddr())
|
||||
h.log.V(6).Info("Bind request", "binddn", bindDN, "basedn", h.backend.BaseDN, "src", conn.RemoteAddr())
|
||||
|
||||
stats.Frontend.Add("bind_reqs", 1)
|
||||
|
||||
// parse the bindDN - ensure that the bindDN ends with the BaseDN
|
||||
if !strings.HasSuffix(bindDN, baseDN) {
|
||||
h.log.V(2).Info("BindDN not part of our BaseDN", "binddn", bindDN, "basedn", h.cfg.Backend.BaseDN)
|
||||
h.log.V(2).Info("BindDN not part of our BaseDN", "binddn", bindDN, "basedn", h.backend.BaseDN)
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
parts := strings.Split(strings.TrimSuffix(bindDN, baseDN), ",")
|
||||
@@ -57,7 +58,7 @@ func (h ownCloudHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.
|
||||
|
||||
// try to login
|
||||
if !h.login(userName, bindSimplePw) {
|
||||
h.log.V(2).Info("Login failed", "username", userName, "basedn", h.cfg.Backend.BaseDN)
|
||||
h.log.V(2).Info("Login failed", "username", userName, "basedn", h.backend.BaseDN)
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
@@ -67,20 +68,20 @@ func (h ownCloudHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.
|
||||
log: h.log,
|
||||
user: userName,
|
||||
password: bindSimplePw,
|
||||
endpoint: h.cfg.Backend.Servers[0],
|
||||
useGraphAPI: h.cfg.Backend.UseGraphAPI,
|
||||
endpoint: h.backend.Servers[0],
|
||||
useGraphAPI: h.backend.UseGraphAPI,
|
||||
client: h.client,
|
||||
}
|
||||
h.lock.Unlock()
|
||||
|
||||
stats.Frontend.Add("bind_successes", 1)
|
||||
h.log.V(6).Info("Bind success", "binddn", bindDN, "basedn", h.cfg.Backend.BaseDN, "src", conn.RemoteAddr())
|
||||
h.log.V(6).Info("Bind success", "binddn", bindDN, "basedn", h.backend.BaseDN, "src", conn.RemoteAddr())
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
baseDN := strings.ToLower("," + h.cfg.Backend.BaseDN)
|
||||
baseDN := strings.ToLower("," + h.backend.BaseDN)
|
||||
searchBaseDN := strings.ToLower(searchReq.BaseDN)
|
||||
h.log.V(6).Info("Search request", "binddn", bindDN, "basedn", baseDN, "src", conn.RemoteAddr(), "filter", searchReq.Filter)
|
||||
stats.Frontend.Add("search_reqs", 1)
|
||||
@@ -90,10 +91,10 @@ func (h ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, con
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("search error: Anonymous BindDN not allowed %s", bindDN)
|
||||
}
|
||||
if !strings.HasSuffix(bindDN, baseDN) {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("search error: BindDN %s not in our BaseDN %s", bindDN, h.cfg.Backend.BaseDN)
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("search error: BindDN %s not in our BaseDN %s", bindDN, h.backend.BaseDN)
|
||||
}
|
||||
if !strings.HasSuffix(searchBaseDN, h.cfg.Backend.BaseDN) {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("search error: search BaseDN %s is not in our BaseDN %s", searchBaseDN, h.cfg.Backend.BaseDN)
|
||||
if !strings.HasSuffix(searchBaseDN, h.backend.BaseDN) {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("search error: search BaseDN %s is not in our BaseDN %s", searchBaseDN, h.backend.BaseDN)
|
||||
}
|
||||
// return all users in the config file - the LDAP library will filter results for us
|
||||
entries := []*ldap.Entry{}
|
||||
@@ -128,12 +129,12 @@ func (h ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, con
|
||||
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberUid", Values: members})
|
||||
}
|
||||
dn := fmt.Sprintf("cn=%s,%s=groups,%s", *g.ID, h.cfg.Backend.GroupFormat, h.cfg.Backend.BaseDN)
|
||||
dn := fmt.Sprintf("cn=%s,%s=groups,%s", *g.ID, h.backend.GroupFormat, h.backend.BaseDN)
|
||||
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
|
||||
}
|
||||
case "posixaccount", "":
|
||||
userName := ""
|
||||
if searchBaseDN != strings.ToLower(h.cfg.Backend.BaseDN) {
|
||||
if searchBaseDN != strings.ToLower(h.backend.BaseDN) {
|
||||
parts := strings.Split(strings.TrimSuffix(searchBaseDN, baseDN), ",")
|
||||
if len(parts) >= 1 {
|
||||
userName = strings.TrimPrefix(parts[0], "cn=")
|
||||
@@ -158,7 +159,7 @@ func (h ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, con
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"posixAccount"}})
|
||||
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s from ownCloud", *u.ID)}})
|
||||
dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.cfg.Backend.NameFormat, *u.ID, h.cfg.Backend.GroupFormat, "users", h.cfg.Backend.BaseDN)
|
||||
dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.backend.NameFormat, *u.ID, h.backend.GroupFormat, "users", h.backend.BaseDN)
|
||||
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
|
||||
}
|
||||
}
|
||||
@@ -182,6 +183,10 @@ func (h ownCloudHandler) Delete(boundDN string, deleteDN string, conn net.Conn)
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
func (h ownCloudHandler) FindUser(userName string) (found bool, user config.User, err error) {
|
||||
return false, config.User{}, nil
|
||||
}
|
||||
|
||||
func (h ownCloudHandler) Close(boundDN string, conn net.Conn) error {
|
||||
conn.Close() // close connection to the server when then client is closed
|
||||
h.lock.Lock()
|
||||
@@ -194,12 +199,12 @@ func (h ownCloudHandler) Close(boundDN string, conn net.Conn) error {
|
||||
|
||||
func (h ownCloudHandler) login(name, pw string) bool {
|
||||
var req *http.Request
|
||||
if h.cfg.Backend.UseGraphAPI {
|
||||
if h.backend.UseGraphAPI {
|
||||
// TODO oc10 graphapi app should implement /me
|
||||
req, _ = http.NewRequest("GET", h.cfg.Backend.Servers[0]+"/users/"+name, nil)
|
||||
req, _ = http.NewRequest("GET", h.backend.Servers[0]+"/users/"+name, nil)
|
||||
} else {
|
||||
// use provisioning api
|
||||
meURL := fmt.Sprintf("%s/ocs/v2.php/cloud/user?format=json", h.cfg.Backend.Servers[0])
|
||||
meURL := fmt.Sprintf("%s/ocs/v2.php/cloud/user?format=json", h.backend.Servers[0])
|
||||
req, _ = http.NewRequest("GET", meURL, nil)
|
||||
}
|
||||
req.SetBasicAuth(name, pw)
|
||||
@@ -343,6 +348,7 @@ func NewOwnCloudHandler(opts ...Option) Handler {
|
||||
options := newOptions(opts...)
|
||||
|
||||
return ownCloudHandler{
|
||||
backend: options.Backend,
|
||||
log: options.Logger,
|
||||
cfg: options.Config,
|
||||
sessions: make(map[string]ownCloudSession),
|
||||
@@ -352,7 +358,7 @@ func NewOwnCloudHandler(opts ...Option) Handler {
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: options.Config.Backend.Insecure,
|
||||
InsecureSkipVerify: options.Backend.Insecure,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#####################
|
||||
# Plugins Targets
|
||||
#####################
|
||||
|
||||
# Note: to make a plugin compatible with a binary built in debug mode, add `-gcflags='all=-N -l'`
|
||||
|
||||
plugins: plugin_mysql plugin_postgres plugin_sqlite
|
||||
|
||||
plugin_mysql: bin/mysql.so
|
||||
|
||||
bin/mysql.so: pkg/plugins/mysql.go pkg/plugins/basesqlhandler.go
|
||||
GOOS=linux GOARCH=amd64 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -buildmode=plugin -o $@ $^
|
||||
|
||||
plugin_postgres: bin/postgres.so
|
||||
|
||||
bin/postgres.so: pkg/plugins/postgres.go pkg/plugins/basesqlhandler.go
|
||||
GOOS=linux GOARCH=amd64 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -buildmode=plugin -o $@ $^
|
||||
|
||||
plugin_sqlite: bin/sqlite.so
|
||||
|
||||
bin/sqlite.so: pkg/plugins/sqlite.go pkg/plugins/basesqlhandler.go
|
||||
GOOS=linux GOARCH=amd64 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -buildmode=plugin -o $@ $^
|
||||
@@ -0,0 +1,132 @@
|
||||
# GLAuth Plugins
|
||||
|
||||
This folder contains plugins; that is, backends that are not compiled in GLAuth by default.
|
||||
|
||||
To quote 'Butonic' (Jörn Friedrich Dreyer):
|
||||
|
||||
> Just keep the 'lightweight' in mind.
|
||||
|
||||
To build either back-end, type
|
||||
```
|
||||
make plugin_name
|
||||
```
|
||||
where 'name' is the plugin's name; so, for instance: `make plugin_sqlite`
|
||||
|
||||
## Database Plugins
|
||||
|
||||
To use a database plugin, edit the configuration file (see pkg/plugins/sample-database.cfg) so that:
|
||||
|
||||
```
|
||||
...
|
||||
[backend]
|
||||
datastore = "plugin"
|
||||
plugin = "dynamic library you created using the previous 'make' command"
|
||||
database = "database connection string"
|
||||
...
|
||||
```
|
||||
so, let's say you built the 'sqlite' plugin, you would now specify its library: `database = sqlite.so`
|
||||
|
||||
### SQLite, MySQL, Postgres
|
||||
|
||||
Tables:
|
||||
- users, groups are self-explanatory
|
||||
- includegroups store the 'includegroups' relationships
|
||||
- othergroups, on the other hand, are a comma-separated list found in the users table (performance)
|
||||
|
||||
Here is how to insert example data using your database's REPL (more detailed information can be found in pkg/plugins/sample-database.cfg)
|
||||
|
||||
```sql
|
||||
INSERT INTO users(name, unixid, primarygroup, passsha256) VALUES('hackers', 5001, 5501, '6478579e37aff45f013e14eeb30b3cc56c72ccdc310123bcdf53e0333e3f416a');
|
||||
INSERT INTO groups(name, unixid) VALUES('superheros', 5501);
|
||||
INSERT INTO groups(name, unixid) VALUES('crusaders', 5502);
|
||||
INSERT INTO groups(name, unixid) VALUES('civilians', 5503);
|
||||
INSERT INTO groups(name, unixid) VALUES('caped', 5504);
|
||||
INSERT INTO groups(name, unixid) VALUES('lovesailing', 5505);
|
||||
INSERT INTO groups(name, unixid) VALUES('smoker', 5506);
|
||||
INSERT INTO includegroups(parentgroupid, includegroupid) VALUES(5503, 5501);
|
||||
INSERT INTO includegroups(parentgroupid, includegroupid) VALUES(5504, 5502);
|
||||
INSERT INTO includegroups(parentgroupid, includegroupid) VALUES(5504, 5501);
|
||||
INSERT INTO users(name, unixid, primarygroup, passsha256) VALUES('user1', 5001, 5501, '6478579e37aff45f013e14eeb30b3cc56c72ccdc310123bcdf53e0333e3f416a');
|
||||
INSERT INTO users(name, unixid, primarygroup, passsha256) VALUES('user2', 5002, 5502, '6478579e37aff45f013e14eeb30b3cc56c72ccdc310123bcdf53e0333e3f416a');
|
||||
INSERT INTO users(name, unixid, primarygroup, passsha256) VALUES('user3', 5003, 5504, '6478579e37aff45f013e14eeb30b3cc56c72ccdc310123bcdf53e0333e3f416a');
|
||||
INSERT INTO users(name, unixid, primarygroup, passsha256, othergroups) VALUES('user4', 5004, 5504, '6478579e37aff45f013e14eeb30b3cc56c72ccdc310123bcdf53e0333e3f416a', '5505,5506');
|
||||
```
|
||||
This should be equivalent to this configuration:
|
||||
```text
|
||||
[[users]]
|
||||
name = "hackers"
|
||||
unixid = 5001
|
||||
primarygroup = 5501
|
||||
passsha256 = "6478579e37aff45f013e14eeb30b3cc56c72ccdc310123bcdf53e0333e3f416a" # dogood
|
||||
|
||||
[[users]]
|
||||
name = "user1"
|
||||
unixid = 5001
|
||||
primarygroup = 5501
|
||||
passsha256 = "6478579e37aff45f013e14eeb30b3cc56c72ccdc310123bcdf53e0333e3f416a" # dogood
|
||||
|
||||
[[users]]
|
||||
name = "user2"
|
||||
unixid = 5002
|
||||
primarygroup = 5502
|
||||
passsha256 = "6478579e37aff45f013e14eeb30b3cc56c72ccdc310123bcdf53e0333e3f416a" # dogood
|
||||
|
||||
[[users]]
|
||||
name = "user3"
|
||||
unixid = 5003
|
||||
primarygroup = 5504
|
||||
passsha256 = "6478579e37aff45f013e14eeb30b3cc56c72ccdc310123bcdf53e0333e3f416a" # dogood
|
||||
|
||||
[[users]]
|
||||
name = "user4"
|
||||
unixid = 5003
|
||||
primarygroup = 5504
|
||||
othergroups = [5505, 5506]
|
||||
passsha256 = "6478579e37aff45f013e14eeb30b3cc56c72ccdc310123bcdf53e0333e3f416a" # dogood
|
||||
|
||||
[[groups]]
|
||||
name = "superheros"
|
||||
unixid = 5501
|
||||
|
||||
[[groups]]
|
||||
name = "crusaders"
|
||||
unixid = 5502
|
||||
|
||||
[[groups]]
|
||||
name = "civilians"
|
||||
unixid = 5503
|
||||
includegroups = [ 5501 ]
|
||||
|
||||
[[groups]]
|
||||
name = "caped"
|
||||
unixid = 5504
|
||||
includegroups = [ 5502, 5501 ]
|
||||
```
|
||||
and LDAP should return these `memberOf` values:
|
||||
```text
|
||||
uid: user1
|
||||
ou: superheros
|
||||
memberOf: cn=caped,ou=groups,dc=militate,dc=com
|
||||
memberOf: cn=civilians,ou=groups,dc=militate,dc=com
|
||||
memberOf: cn=superheros,ou=groups,dc=militate,dc=com
|
||||
|
||||
uid: user2
|
||||
ou: crusaders
|
||||
memberOf: cn=caped,ou=groups,dc=militate,dc=com
|
||||
memberOf: cn=crusaders,ou=groups,dc=militate,dc=com
|
||||
|
||||
uid: user3
|
||||
ou: caped
|
||||
memberOf: cn=caped,ou=groups,dc=militate,dc=com
|
||||
|
||||
uid: user4
|
||||
ou: caped
|
||||
memberOf: cn=caped,ou=groups,dc=militate,dc=com
|
||||
memberOf: cn=lovesailing,ou=groups,dc=militate,dc=com
|
||||
memberOf: cn=smoker,ou=groups,dc=militate,dc=com
|
||||
```
|
||||
If you have the ldap client package installed, this can be easily confirmed by running
|
||||
```
|
||||
ldapsearch -H ldap://localhost:3893 -D cn=hackers,ou=superheros,dc=glauth,dc=com -w dogood -x -bdc=glauth,dc=com cn=user1
|
||||
```
|
||||
and so on.
|
||||
@@ -0,0 +1,597 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/GeertJohan/yubigo"
|
||||
"github.com/glauth/glauth/pkg/config"
|
||||
"github.com/glauth/glauth/pkg/handler"
|
||||
"github.com/glauth/glauth/pkg/stats"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/nmcclain/ldap"
|
||||
"github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
type SqlBackend interface {
|
||||
// Name used by database/sql when loading the driver
|
||||
GetDriverName() string
|
||||
// Create db/schema if necessary
|
||||
CreateSchema(db *sql.DB)
|
||||
//
|
||||
GetPrepareSymbol() string
|
||||
}
|
||||
|
||||
type database struct {
|
||||
path string
|
||||
cnx *sql.DB
|
||||
}
|
||||
|
||||
type databaseHandler struct {
|
||||
backend config.Backend
|
||||
log logr.Logger
|
||||
cfg *config.Config
|
||||
yubikeyAuth *yubigo.YubiAuth
|
||||
sqlBackend SqlBackend
|
||||
database database
|
||||
MemGroups []config.Group
|
||||
}
|
||||
|
||||
// func NewDatabaseHandler_deprecated(log *logging.Logger, cfg *config.Config, yubikeyAuth *yubigo.YubiAuth, sqlBackend SqlBackend) handler.Handler {
|
||||
func NewDatabaseHandler(sqlBackend SqlBackend, opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
|
||||
// Note: we will never terminate this connection pool.
|
||||
db, err := sql.Open(sqlBackend.GetDriverName(), options.Backend.Database)
|
||||
if err != nil {
|
||||
options.Logger.Error(err, "Unable to open SQL database named '%s' error: %s", options.Backend.Database)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = db.Ping()
|
||||
if err != nil {
|
||||
options.Logger.Error(err, "Unable to communicate with SQL database error: %s", options.Backend.Database)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dbInfo := database{
|
||||
path: options.Backend.Database,
|
||||
cnx: db,
|
||||
}
|
||||
|
||||
handler := databaseHandler{
|
||||
backend: options.Backend,
|
||||
log: options.Logger,
|
||||
cfg: options.Config,
|
||||
yubikeyAuth: options.YubiAuth,
|
||||
sqlBackend: sqlBackend,
|
||||
database: dbInfo}
|
||||
|
||||
sqlBackend.CreateSchema(db)
|
||||
|
||||
options.Logger.V(3).Info("Database (" + sqlBackend.GetDriverName() + "::" + options.Backend.Database + ") Plugin: Ready")
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func (h databaseHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCode ldap.LDAPResultCode, err error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
baseDN := strings.ToLower("," + h.backend.BaseDN)
|
||||
|
||||
h.log.V(3).Info(fmt.Sprintf("Bind request: bindDN: %s, BaseDN: %s, source: %s", bindDN, h.backend.BaseDN, conn.RemoteAddr().String()))
|
||||
|
||||
stats.Frontend.Add("bind_reqs", 1)
|
||||
|
||||
// parse the bindDN - ensure that the bindDN ends with the BaseDN
|
||||
if !strings.HasSuffix(bindDN, baseDN) {
|
||||
h.log.V(2).Info(fmt.Sprintf("Bind Error: BindDN %s not our BaseDN %s", bindDN, h.backend.BaseDN))
|
||||
// h.log.V(2).Info(fmt.Sprintf("Bind Error: BindDN %s not our BaseDN %s", bindDN, baseDN))
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
parts := strings.Split(strings.TrimSuffix(bindDN, baseDN), ",")
|
||||
groupName := ""
|
||||
userName := ""
|
||||
if len(parts) == 1 {
|
||||
userName = strings.TrimPrefix(parts[0], h.backend.NameFormat+"=")
|
||||
} else if len(parts) == 2 {
|
||||
userName = strings.TrimPrefix(parts[0], h.backend.NameFormat+"=")
|
||||
groupName = strings.TrimPrefix(parts[1], h.backend.GroupFormat+"=")
|
||||
} else {
|
||||
h.log.V(2).Info(fmt.Sprintf("Bind Error: BindDN %s should have only one or two parts (has %d)", bindDN, len(parts)))
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
// find the user
|
||||
user := config.User{}
|
||||
err = h.database.cnx.QueryRow(fmt.Sprintf(`
|
||||
SELECT u.unixid,u.primarygroup,u.passsha256,u.otpsecret,u.yubikey
|
||||
FROM users u WHERE lower(u.name)=%s`, h.sqlBackend.GetPrepareSymbol()), userName).Scan(
|
||||
&user.UnixID, &user.PrimaryGroup, &user.PassSHA256, &user.OTPSecret, &user.Yubikey)
|
||||
if err != nil {
|
||||
h.log.V(2).Info(fmt.Sprintf("Bind Error: User %s not found.", userName))
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
// find the group
|
||||
group := config.Group{}
|
||||
err = h.database.cnx.QueryRow(fmt.Sprintf(`
|
||||
SELECT g.unixid FROM groups g WHERE lower(name)=%s`, h.sqlBackend.GetPrepareSymbol()), groupName).Scan(
|
||||
&group.UnixID)
|
||||
if err != nil {
|
||||
h.log.V(2).Info(fmt.Sprintf("Bind Error: Group %s not found.", userName))
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
// validate group membership
|
||||
if user.PrimaryGroup != group.UnixID {
|
||||
h.log.V(2).Info(fmt.Sprintf("Bind Error: User %s primary group is not %s.", userName, groupName))
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
validotp := false
|
||||
|
||||
if len(user.Yubikey) == 0 && len(user.OTPSecret) == 0 {
|
||||
validotp = true
|
||||
}
|
||||
|
||||
if len(user.Yubikey) > 0 && h.yubikeyAuth != nil {
|
||||
if len(bindSimplePw) > 44 {
|
||||
otp := bindSimplePw[len(bindSimplePw)-44:]
|
||||
yubikeyid := otp[0:12]
|
||||
bindSimplePw = bindSimplePw[:len(bindSimplePw)-44]
|
||||
|
||||
if user.Yubikey == yubikeyid {
|
||||
_, ok, _ := h.yubikeyAuth.Verify(otp)
|
||||
|
||||
if ok {
|
||||
validotp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the full bind password provided before possibly modifying
|
||||
// in the otp check
|
||||
// Generate a hash of the provided password
|
||||
hashFull := sha256.New()
|
||||
hashFull.Write([]byte(bindSimplePw))
|
||||
|
||||
// Test OTP if exists
|
||||
if len(user.OTPSecret) > 0 && !validotp {
|
||||
if len(bindSimplePw) > 6 {
|
||||
otp := bindSimplePw[len(bindSimplePw)-6:]
|
||||
bindSimplePw = bindSimplePw[:len(bindSimplePw)-6]
|
||||
|
||||
validotp = totp.Validate(otp, user.OTPSecret)
|
||||
}
|
||||
}
|
||||
|
||||
// finally, validate user's pw
|
||||
|
||||
// check app passwords first
|
||||
for index, appPw := range user.PassAppSHA256 {
|
||||
|
||||
if appPw != hex.EncodeToString(hashFull.Sum(nil)) {
|
||||
h.log.V(2).Info(fmt.Sprintf("Attempted to bind app pw #%d - failure as %s from %s", index, bindDN, conn.RemoteAddr().String()))
|
||||
} else {
|
||||
stats.Frontend.Add("bind_successes", 1)
|
||||
h.log.V(3).Info("Bind success using app pw #%d as %s from %s", index, bindDN, conn.RemoteAddr().String())
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// then check main password with the hash
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(bindSimplePw))
|
||||
|
||||
// Then ensure the OTP is valid before checking
|
||||
if !validotp {
|
||||
h.log.V(2).Info(fmt.Sprintf("Bind Error: invalid OTP token as %s from %s", bindDN, conn.RemoteAddr().String()))
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
// Now, check the hash
|
||||
if user.PassSHA256 != hex.EncodeToString(hash.Sum(nil)) {
|
||||
h.log.V(2).Info(fmt.Sprintf("Bind Error: invalid credentials as %s from %s", bindDN, conn.RemoteAddr().String()))
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
stats.Frontend.Add("bind_successes", 1)
|
||||
h.log.V(3).Info(fmt.Sprintf("Bind success as %s from %s", bindDN, conn.RemoteAddr().String()))
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h databaseHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
baseDN := strings.ToLower("," + h.backend.BaseDN)
|
||||
searchBaseDN := strings.ToLower(searchReq.BaseDN)
|
||||
h.log.V(3).Info(fmt.Sprintf("Search request as %s from %s for %s", bindDN, conn.RemoteAddr().String(), searchReq.Filter))
|
||||
stats.Frontend.Add("search_reqs", 1)
|
||||
|
||||
// validate the user is authenticated and has appropriate access
|
||||
if len(bindDN) < 1 {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", bindDN)
|
||||
}
|
||||
if !strings.HasSuffix(bindDN, baseDN) {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", bindDN, h.backend.BaseDN)
|
||||
}
|
||||
if !strings.HasSuffix(searchBaseDN, h.backend.BaseDN) {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: search BaseDN %s is not in our BaseDN %s", searchBaseDN, h.backend.BaseDN)
|
||||
}
|
||||
// return all users in the config file - the LDAP library will filter results for us
|
||||
entries := []*ldap.Entry{}
|
||||
filterEntity, err := ldap.GetFilterObjectClass(searchReq.Filter)
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", searchReq.Filter)
|
||||
}
|
||||
switch filterEntity {
|
||||
default:
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, searchReq.Filter)
|
||||
case "posixgroup":
|
||||
h.MemGroups, err = h.memoizeGroups()
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: Unable to memoize groups [%s]", err.Error())
|
||||
}
|
||||
|
||||
for _, g := range h.MemGroups {
|
||||
entries = append(entries, h.getGroup(g))
|
||||
}
|
||||
case "posixaccount", "":
|
||||
h.MemGroups, err = h.memoizeGroups()
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: Unable to memoize groups [%s]", err.Error())
|
||||
}
|
||||
|
||||
rows, err := h.database.cnx.Query(`
|
||||
SELECT u.name,u.unixid,u.primarygroup,u.passsha256,u.otpsecret,u.yubikey,u.othergroups,u.givenname,u.sn,u.mail,u.loginshell,u.homedirectory,u.disabled
|
||||
FROM users u`)
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: Unable to retrieve data [%s]", err.Error())
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var otherGroups string
|
||||
var disabled int
|
||||
u := config.User{}
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&u.Name, &u.UnixID, &u.PrimaryGroup, &u.PassSHA256, &u.OTPSecret, &u.Yubikey, &otherGroups, &u.GivenName, &u.SN, &u.Mail, &u.LoginShell, &u.Homedir, &disabled)
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: Unable to retrieve data [%s]", err.Error())
|
||||
}
|
||||
u.OtherGroups = h.commaListToTable(otherGroups)
|
||||
u.Disabled = h.intToBool(disabled)
|
||||
|
||||
entries = append(entries, h.getAccount(u))
|
||||
}
|
||||
}
|
||||
stats.Frontend.Add("search_successes", 1)
|
||||
h.log.V(3).Info(fmt.Sprintf("AP: Search OK: %s", searchReq.Filter))
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
|
||||
// Add is not yet supported for the sql backend
|
||||
func (h databaseHandler) Add(boundDN string, req ldap.AddRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
// Modify is not yet supported for the sql backend
|
||||
func (h databaseHandler) Modify(boundDN string, req ldap.ModifyRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
// Delete is not yet supported for the sql backend
|
||||
func (h databaseHandler) Delete(boundDN string, deleteDN string, conn net.Conn) (result ldap.LDAPResultCode, err error) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
func (h databaseHandler) FindUser(userName string) (f bool, u config.User, err error) {
|
||||
user := config.User{}
|
||||
found := false
|
||||
|
||||
err = h.database.cnx.QueryRow(fmt.Sprintf(`
|
||||
SELECT u.unixid,u.primarygroup,u.passsha256,u.otpsecret,u.yubikey
|
||||
FROM users u WHERE lower(u.name)=%s`, h.sqlBackend.GetPrepareSymbol()), userName).Scan(
|
||||
&user.UnixID, &user.PrimaryGroup, &user.PassSHA256, &user.OTPSecret, &user.Yubikey)
|
||||
if err == nil {
|
||||
found = true
|
||||
}
|
||||
|
||||
return found, user, err
|
||||
}
|
||||
|
||||
func (h databaseHandler) Close(boundDn string, conn net.Conn) error {
|
||||
stats.Frontend.Add("closes", 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h databaseHandler) intToBool(value int) bool {
|
||||
if value == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (h databaseHandler) commaListToTable(commaList string) []int {
|
||||
if len(commaList) == 0 {
|
||||
return make([]int, 0)
|
||||
}
|
||||
rowsAsStrings := strings.Split(commaList, ",")
|
||||
rowsAsInts := make([]int, len(rowsAsStrings))
|
||||
for i, v := range rowsAsStrings {
|
||||
iv, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return rowsAsInts
|
||||
}
|
||||
rowsAsInts[i] = iv
|
||||
}
|
||||
return rowsAsInts
|
||||
}
|
||||
|
||||
func (h databaseHandler) memoizeGroups() ([]config.Group, error) {
|
||||
workMemGroups := make([]*config.Group, 0)
|
||||
rows, err := h.database.cnx.Query(`
|
||||
SELECT g1.name,g1.unixid,ig.includegroupid
|
||||
FROM groups g1
|
||||
LEFT JOIN includegroups ig ON g1.unixid=ig.parentgroupid
|
||||
LEFT JOIN groups g2 ON ig.includegroupid=g2.unixid`)
|
||||
if err != nil {
|
||||
return nil, errors.New("Unable to memoize groups list")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var groupName string
|
||||
var groupId int
|
||||
var includeId sql.NullInt64 // Store includeid from left join
|
||||
var pg *config.Group
|
||||
recentId := -1 // id of recently updated group
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&groupName, &groupId, &includeId)
|
||||
if err != nil {
|
||||
return nil, errors.New("Unable to memoize groups list")
|
||||
}
|
||||
if recentId != groupId {
|
||||
recentId = groupId
|
||||
g := config.Group{Name: groupName, UnixID: groupId}
|
||||
pg = &g // To manipulate end of slice
|
||||
workMemGroups = append(workMemGroups, &g)
|
||||
}
|
||||
if includeId.Valid {
|
||||
pg.IncludeGroups = append(pg.IncludeGroups, int(includeId.Int64))
|
||||
}
|
||||
}
|
||||
memGroups := make([]config.Group, len(workMemGroups))
|
||||
for i, v := range workMemGroups {
|
||||
memGroups[i] = config.Group{Name: v.Name, UnixID: v.UnixID, IncludeGroups: v.IncludeGroups}
|
||||
}
|
||||
return memGroups, nil
|
||||
}
|
||||
|
||||
// Used when looking up Posix Groups
|
||||
func (h databaseHandler) getGroupMembers(gid int) []string {
|
||||
members := make(map[string]bool)
|
||||
|
||||
rows, err := h.database.cnx.Query(`
|
||||
SELECT u.name,u.unixid,u.primarygroup,u.passsha256,u.otpsecret,u.yubikey,u.othergroups
|
||||
FROM users u WHERE lower(u.name)=?`,
|
||||
)
|
||||
if err != nil {
|
||||
// Silent fail... for now
|
||||
return []string{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var otherGroups string
|
||||
u := config.User{}
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&u.Name, &u.UnixID, &u.PrimaryGroup, &u.PassSHA256, &u.OTPSecret, &u.Yubikey, &otherGroups)
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
if u.PrimaryGroup == gid {
|
||||
dn := fmt.Sprintf("cn=%s,ou=%s,%s", u.Name, h.getGroupName(u.PrimaryGroup), h.backend.BaseDN)
|
||||
members[dn] = true
|
||||
} else {
|
||||
u.OtherGroups = h.commaListToTable(otherGroups)
|
||||
for _, othergid := range u.OtherGroups {
|
||||
if othergid == gid {
|
||||
dn := fmt.Sprintf("cn=%s,ou=%s,%s", u.Name, h.getGroupName(u.PrimaryGroup), h.backend.BaseDN)
|
||||
members[dn] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, g := range h.MemGroups {
|
||||
if gid == g.UnixID {
|
||||
for _, includegroupid := range g.IncludeGroups {
|
||||
if includegroupid != gid {
|
||||
includegroupmembers := h.getGroupMembers(includegroupid)
|
||||
|
||||
for _, includegroupmember := range includegroupmembers {
|
||||
members[includegroupmember] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m := []string{}
|
||||
for k, _ := range members {
|
||||
m = append(m, k)
|
||||
}
|
||||
|
||||
sort.Strings(m)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Used exclusively when looking up Posix Groups
|
||||
func (h databaseHandler) getGroupMemberIDs(gid int) []string {
|
||||
members := make(map[string]bool)
|
||||
rows, err := h.database.cnx.Query(`
|
||||
SELECT u.name,u.unixid,u.primarygroup,u.passsha256,u.otpsecret,u.yubikey,u.othergroups
|
||||
FROM users u`)
|
||||
if err != nil {
|
||||
// Silent fail... for now
|
||||
return []string{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var otherGroups string
|
||||
u := config.User{}
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&u.Name, &u.UnixID, &u.PrimaryGroup, &u.PassSHA256, &u.OTPSecret, &u.Yubikey, &otherGroups)
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
if u.PrimaryGroup == gid {
|
||||
members[u.Name] = true
|
||||
} else {
|
||||
u.OtherGroups = h.commaListToTable(otherGroups)
|
||||
for _, othergid := range u.OtherGroups {
|
||||
if othergid == gid {
|
||||
members[u.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, g := range h.MemGroups {
|
||||
if gid == g.UnixID {
|
||||
for _, includegroupid := range g.IncludeGroups {
|
||||
if includegroupid == gid {
|
||||
h.log.V(3).Info(fmt.Sprintf("Group: %d - Ignoring myself as included group", includegroupid))
|
||||
} else {
|
||||
includegroupmemberids := h.getGroupMemberIDs(includegroupid)
|
||||
|
||||
for _, includegroupmemberid := range includegroupmemberids {
|
||||
members[includegroupmemberid] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m := []string{}
|
||||
for k, _ := range members {
|
||||
m = append(m, k)
|
||||
}
|
||||
|
||||
sort.Strings(m)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Invoked for every user being returned from our database
|
||||
func (h databaseHandler) getGroupDNs(gids []int) []string {
|
||||
groups := make(map[string]bool)
|
||||
for _, gid := range gids {
|
||||
for _, g := range h.MemGroups {
|
||||
if g.UnixID == gid {
|
||||
dn := fmt.Sprintf("cn=%s,ou=groups,%s", g.Name, h.backend.BaseDN)
|
||||
groups[dn] = true
|
||||
}
|
||||
|
||||
for _, includegroupid := range g.IncludeGroups {
|
||||
if includegroupid == gid && g.UnixID != gid {
|
||||
includegroupdns := h.getGroupDNs([]int{g.UnixID})
|
||||
|
||||
for _, includegroupdn := range includegroupdns {
|
||||
groups[includegroupdn] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g := []string{}
|
||||
for k, _ := range groups {
|
||||
g = append(g, k)
|
||||
}
|
||||
|
||||
sort.Strings(g)
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// Invoked for every user being returned from our database
|
||||
func (h databaseHandler) getGroupName(gid int) string {
|
||||
for _, g := range h.MemGroups {
|
||||
if g.UnixID == gid {
|
||||
return g.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Toolbox
|
||||
func (h databaseHandler) getGroup(g config.Group) *ldap.Entry {
|
||||
attrs := []*ldap.EntryAttribute{}
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"cn", []string{g.Name}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"description", []string{fmt.Sprintf("%s via LDAP", g.Name)}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"gidNumber", []string{fmt.Sprintf("%d", g.UnixID)}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"objectClass", []string{"posixGroup"}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"uniqueMember", h.getGroupMembers(g.UnixID)})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"memberUid", h.getGroupMemberIDs(g.UnixID)})
|
||||
dn := fmt.Sprintf("cn=%s,ou=groups,%s", g.Name, h.backend.BaseDN)
|
||||
return &ldap.Entry{dn, attrs}
|
||||
}
|
||||
|
||||
func (h databaseHandler) getAccount(u config.User) *ldap.Entry {
|
||||
attrs := []*ldap.EntryAttribute{}
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"cn", []string{u.Name}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"uid", []string{u.Name}})
|
||||
|
||||
if len(u.GivenName) > 0 {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"givenName", []string{u.GivenName}})
|
||||
}
|
||||
|
||||
if len(u.SN) > 0 {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"sn", []string{u.SN}})
|
||||
}
|
||||
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"ou", []string{h.getGroupName(u.PrimaryGroup)}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"uidNumber", []string{fmt.Sprintf("%d", u.UnixID)}})
|
||||
|
||||
if u.Disabled {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"accountStatus", []string{"inactive"}})
|
||||
} else {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"accountStatus", []string{"active"}})
|
||||
}
|
||||
|
||||
if len(u.Mail) > 0 {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"mail", []string{u.Mail}})
|
||||
}
|
||||
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"objectClass", []string{"posixAccount"}})
|
||||
|
||||
if len(u.LoginShell) > 0 {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"loginShell", []string{u.LoginShell}})
|
||||
} else {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"loginShell", []string{"/bin/bash"}})
|
||||
}
|
||||
|
||||
if len(u.Homedir) > 0 {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"homeDirectory", []string{u.Homedir}})
|
||||
} else {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"homeDirectory", []string{"/home/" + u.Name}})
|
||||
}
|
||||
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"description", []string{fmt.Sprintf("%s via LDAP", u.Name)}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"gecos", []string{fmt.Sprintf("%s via LDAP", u.Name)}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"gidNumber", []string{fmt.Sprintf("%d", u.PrimaryGroup)}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"memberOf", h.getGroupDNs(append(u.OtherGroups, u.PrimaryGroup))})
|
||||
if len(u.SSHKeys) > 0 {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{"sshPublicKey", u.SSHKeys})
|
||||
}
|
||||
dn := fmt.Sprintf("cn=%s,ou=%s,%s", u.Name, h.getGroupName(u.PrimaryGroup), h.backend.BaseDN)
|
||||
return &ldap.Entry{dn, attrs}
|
||||
}
|
||||
|
||||
func main() {}
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/glauth/glauth/pkg/handler"
|
||||
)
|
||||
|
||||
type MysqlBackend struct {
|
||||
}
|
||||
|
||||
func NewMySQLHandler(opts ...handler.Option) handler.Handler {
|
||||
backend := MysqlBackend{}
|
||||
return NewDatabaseHandler(backend, opts...)
|
||||
}
|
||||
|
||||
func (b MysqlBackend) GetDriverName() string {
|
||||
return "mysql"
|
||||
}
|
||||
|
||||
func (b MysqlBackend) GetPrepareSymbol() string {
|
||||
return "?"
|
||||
}
|
||||
|
||||
// Create db/schema if necessary
|
||||
func (b MysqlBackend) CreateSchema(db *sql.DB) {
|
||||
statement, _ := db.Prepare(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
unixid INTEGER NOT NULL,
|
||||
primarygroup INTEGER NOT NULL,
|
||||
othergroups VARCHAR(1024) DEFAULT '',
|
||||
givenname VARCHAR(64) DEFAULT '',
|
||||
sn VARCHAR(64) DEFAULT '',
|
||||
mail VARCHAR(254) DEFAULT '',
|
||||
loginshell VARCHAR(64) DEFAULT '',
|
||||
homedirectory VARCHAR(64) DEFAULT '',
|
||||
disabled SMALLINT DEFAULT 0,
|
||||
passsha256 VARCHAR(64) DEFAULT '',
|
||||
otpsecret VARCHAR(64) DEFAULT '',
|
||||
yubikey VARCHAR(128) DEFAULT '')
|
||||
`)
|
||||
statement.Exec()
|
||||
statement, _ = db.Prepare("CREATE UNIQUE INDEX idx_user_name on users(name)")
|
||||
statement.Exec()
|
||||
statement, _ = db.Prepare("CREATE TABLE IF NOT EXISTS groups (id INTEGER AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, unixid INTEGER NOT NULL)")
|
||||
statement.Exec()
|
||||
statement, _ = db.Prepare("CREATE UNIQUE INDEX idx_group_name on groups(name)")
|
||||
statement.Exec()
|
||||
statement, _ = db.Prepare("CREATE TABLE IF NOT EXISTS includegroups (id INTEGER AUTO_INCREMENT PRIMARY KEY, parentgroupid INTEGER NOT NULL, includegroupid INTEGER NOT NULL)")
|
||||
statement.Exec()
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"github.com/glauth/glauth/pkg/handler"
|
||||
)
|
||||
|
||||
type PostgresBackend struct {
|
||||
}
|
||||
|
||||
func NewPostgresHandler(opts ...handler.Option) handler.Handler {
|
||||
backend := PostgresBackend{}
|
||||
return NewDatabaseHandler(backend, opts...)
|
||||
}
|
||||
|
||||
func (b PostgresBackend) GetDriverName() string {
|
||||
return "postgres"
|
||||
}
|
||||
|
||||
func (b PostgresBackend) GetPrepareSymbol() string {
|
||||
return "$1"
|
||||
}
|
||||
|
||||
// Create db/schema if necessary
|
||||
func (b PostgresBackend) CreateSchema(db *sql.DB) {
|
||||
statement, _ := db.Prepare(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
unixid INTEGER NOT NULL,
|
||||
primarygroup INTEGER NOT NULL,
|
||||
othergroups TEXT DEFAULT '',
|
||||
givenname TEXT DEFAULT '',
|
||||
sn TEXT DEFAULT '',
|
||||
mail TEXT DEFAULT '',
|
||||
loginshell TEXT DEFAULT '',
|
||||
homedirectory TEXT DEFAULT '',
|
||||
disabled SMALLINT DEFAULT 0,
|
||||
passsha256 TEXT DEFAULT '',
|
||||
otpsecret TEXT DEFAULT '',
|
||||
yubikey TEXT DEFAULT '')
|
||||
`)
|
||||
statement.Exec()
|
||||
statement, _ = db.Prepare("CREATE UNIQUE INDEX IF NOT EXISTS idx_user_name on users(name)")
|
||||
statement.Exec()
|
||||
statement, _ = db.Prepare("CREATE TABLE IF NOT EXISTS groups (id SERIAL PRIMARY KEY, name TEXT NOT NULL, unixid INTEGER NOT NULL)")
|
||||
statement.Exec()
|
||||
statement, _ = db.Prepare("CREATE UNIQUE INDEX IF NOT EXISTS idx_group_name on groups(name)")
|
||||
statement.Exec()
|
||||
statement, _ = db.Prepare("CREATE TABLE IF NOT EXISTS includegroups (id SERIAL PRIMARY KEY, parentgroupid INTEGER NOT NULL, includegroupid INTEGER NOT NULL)")
|
||||
statement.Exec()
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#################
|
||||
# glauth.conf
|
||||
|
||||
#################
|
||||
# General configuration.
|
||||
debug = true
|
||||
# syslog = true
|
||||
#
|
||||
# Enable hot-reload of configuration on changes
|
||||
# - does NOT work [ldap], [ldaps], [backend] or [api] sections
|
||||
# watchconfig = true
|
||||
|
||||
#################
|
||||
# yubikeyclientid = "yubi-api-clientid"
|
||||
# yubikeysecret = "yubi-api-secret"
|
||||
|
||||
#################
|
||||
# Server configuration.
|
||||
#
|
||||
# This is the old server-config format
|
||||
# - will still work, but please use new format, shown below
|
||||
#
|
||||
# [frontend]
|
||||
# listen = "localhost:8484"
|
||||
# tls = false
|
||||
# cert ="certs/server.crt"
|
||||
# key = "certs/server.key"
|
||||
|
||||
# This ([ldap] and [ldaps]) is the new server-config format
|
||||
[ldap]
|
||||
enabled = true
|
||||
listen = "0.0.0.0:3893"
|
||||
|
||||
[ldaps]
|
||||
enabled = true
|
||||
listen = "0.0.0.0:3894"
|
||||
cert = "certs/server.crt"
|
||||
key = "certs/server.key"
|
||||
|
||||
#################
|
||||
# The backend section controls the data store.
|
||||
[backend]
|
||||
datastore = "plugin"
|
||||
# If "plugin," uncomment the line below
|
||||
plugin = "bin/sqlite.so"
|
||||
pluginhandler = "NewSQLiteHandler"
|
||||
baseDN = "dc=glauth,dc=com"
|
||||
|
||||
# // MySQL //
|
||||
# Uncomment this line to configure your MySQL database connection string:
|
||||
# database = "glauth:glauth@tcp(127.0.0.1:3306)/glauth"
|
||||
# Creating database:
|
||||
# create database glauth identified;
|
||||
# grant all on glauth.* to glauth@localhost identified by 'glauth';
|
||||
# // Postgres //
|
||||
# Uncomment this line to configure your Postgres database connection string:
|
||||
# database = "host=127.0.0.1 port=5432 dbname=glauth user=glauth password=glauth sslmode=disable"
|
||||
# Creating database:
|
||||
# CREATE DATABASE <dbname>;
|
||||
# CREATE USER <username> WITH PASSWORD '<userpassword>'
|
||||
# GRANT ALL PRIVILEGES ON DATABASE <dbname> to <username>;
|
||||
# // SQLite //
|
||||
# Uncomment this line to configure your SQLite database connection string:
|
||||
database = "gl.db"
|
||||
|
||||
## Configure dn format to use structures like
|
||||
## "uid=serviceuser,cn=svcaccts,$BASEDN" instead of "cn=serviceuser,ou=svcaccts,$BASEDN"
|
||||
## to help ease migrations from other LDAP systems
|
||||
# nameformat = "uid"
|
||||
# groupformat = "cn"
|
||||
|
||||
## Configure ssh-key attribute name, default is 'sshPublicKey'
|
||||
# sshkeyattr = "ipaSshPubKey"
|
||||
|
||||
#################
|
||||
# Enable and configure the optional REST API here.
|
||||
[api]
|
||||
enabled = true
|
||||
tls = false # enable TLS for production!!
|
||||
listen = "0.0.0.0:5555"
|
||||
cert = "cert.pem"
|
||||
key = "key.pem"
|
||||
@@ -0,0 +1,45 @@
|
||||
#################
|
||||
# glauth.conf
|
||||
|
||||
#################
|
||||
# General configuration.
|
||||
#debug = true
|
||||
syslog = true
|
||||
|
||||
#################
|
||||
# Server configuration.
|
||||
[ldap]
|
||||
enabled = true
|
||||
listen = "0.0.0.0:389"
|
||||
|
||||
[ldaps]
|
||||
enabled = true
|
||||
listen = "0.0.0.0:636"
|
||||
cert = "glauth.crt"
|
||||
key = "glauth.key"
|
||||
|
||||
#################
|
||||
# The backend section controls the data store.
|
||||
[backend]
|
||||
datastore = "ldap"
|
||||
servers = [ "ldaps://server1:636", "ldaps://server2:636" ]
|
||||
|
||||
# Ignore SSL certificate errors when connecting to backend LDAP servers.
|
||||
#insecure = true
|
||||
|
||||
[helper]
|
||||
enabled = true
|
||||
datastore = "plugin"
|
||||
plugin = "bin/sqlite.so"
|
||||
pluginhandler = "NewSQLiteHandler"
|
||||
database = "glh.db"
|
||||
|
||||
#################
|
||||
# Enable and configure the optional REST API here.
|
||||
[api]
|
||||
enabled = true
|
||||
tls = true
|
||||
listen = "localhost:5555"
|
||||
cert = "glauth.crt"
|
||||
key = "glauth.key"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/glauth/glauth/pkg/handler"
|
||||
)
|
||||
|
||||
type SqliteBackend struct {
|
||||
}
|
||||
|
||||
func NewSQLiteHandler(opts ...handler.Option) handler.Handler {
|
||||
backend := SqliteBackend{}
|
||||
return NewDatabaseHandler(backend, opts...)
|
||||
}
|
||||
|
||||
func (b SqliteBackend) GetDriverName() string {
|
||||
return "sqlite3"
|
||||
}
|
||||
|
||||
func (b SqliteBackend) GetPrepareSymbol() string {
|
||||
return "?"
|
||||
}
|
||||
|
||||
// Create db/schema if necessary
|
||||
func (b SqliteBackend) CreateSchema(db *sql.DB) {
|
||||
statement, _ := db.Prepare(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
unixid INTEGER NOT NULL,
|
||||
primarygroup INTEGER NOT NULL,
|
||||
othergroups TEXT DEFAULT '',
|
||||
givenname TEXT DEFAULT '',
|
||||
sn TEXT DEFAULT '',
|
||||
mail TEXT DEFAULT '',
|
||||
loginshell TYEXT DEFAULT '',
|
||||
homedirectory TEXT DEFAULT '',
|
||||
disabled SMALLINT DEFAULT 0,
|
||||
passsha256 TEXT DEFAULT '',
|
||||
otpsecret TEXT DEFAULT '',
|
||||
yubikey TEXT DEFAULT '')
|
||||
`)
|
||||
statement.Exec()
|
||||
statement, _ = db.Prepare("CREATE UNIQUE INDEX IF NOT EXISTS idx_user_name on users(name)")
|
||||
statement.Exec()
|
||||
statement, _ = db.Prepare("CREATE TABLE IF NOT EXISTS groups (id INTEGER PRIMARY KEY, name TEXT NOT NULL, unixid INTEGER NOT NULL)")
|
||||
statement.Exec()
|
||||
statement, _ = db.Prepare("CREATE UNIQUE INDEX IF NOT EXISTS idx_group_name on groups(name)")
|
||||
statement.Exec()
|
||||
statement, _ = db.Prepare("CREATE TABLE IF NOT EXISTS includegroups (id INTEGER PRIMARY KEY, parentgroupid INTEGER NOT NULL, includegroupid INTEGER NOT NULL)")
|
||||
statement.Exec()
|
||||
}
|
||||
+104
-25
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"plugin"
|
||||
|
||||
"github.com/GeertJohan/yubigo"
|
||||
"github.com/glauth/glauth/pkg/config"
|
||||
@@ -36,34 +37,112 @@ func NewServer(opts ...Option) (*LdapSvc, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// configure the backend
|
||||
var helper handler.Handler
|
||||
|
||||
// instantiate the helper, if any
|
||||
if s.c.Helper.Enabled {
|
||||
switch s.c.Helper.Datastore {
|
||||
case "config":
|
||||
helper = handler.NewConfigHandler(
|
||||
handler.Logger(s.log),
|
||||
handler.Config(s.c),
|
||||
handler.YubiAuth(s.yubiAuth),
|
||||
)
|
||||
case "plugin":
|
||||
plug, err := plugin.Open(s.c.Helper.Plugin)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Unable to load specified helper plugin: %s", err))
|
||||
}
|
||||
nph, err := plug.Lookup(s.c.Helper.PluginHandler)
|
||||
if err != nil {
|
||||
return nil, errors.New("Unable to find 'NewPluginHandler' in loaded helper plugin")
|
||||
}
|
||||
initFunc, ok := nph.(func(...handler.Option) handler.Handler)
|
||||
|
||||
if !ok {
|
||||
return nil, errors.New("Loaded helper plugin lacks a proper NewPluginHandler function")
|
||||
}
|
||||
// Normally, here, we would somehow have imported our plugin into our
|
||||
// handler namespace. Oops?
|
||||
helper = initFunc(
|
||||
handler.Logger(s.log),
|
||||
handler.Config(s.c),
|
||||
handler.YubiAuth(s.yubiAuth),
|
||||
)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported helper %s - must be one of 'config', 'plugin'", s.c.Helper.Datastore)
|
||||
}
|
||||
s.log.V(3).Info("Using helper", "datastore", s.c.Helper.Datastore)
|
||||
}
|
||||
|
||||
backendCounter := -1
|
||||
allHandlers := handler.HandlerWrapper{Handlers: make([]handler.Handler, 10), Count: &backendCounter}
|
||||
|
||||
// configure the backends
|
||||
s.l = ldap.NewServer()
|
||||
s.l.EnforceLDAP = true
|
||||
var h handler.Handler
|
||||
switch s.c.Backend.Datastore {
|
||||
case "ldap":
|
||||
h = handler.NewLdapHandler(
|
||||
handler.Logger(s.log),
|
||||
handler.Config(s.c),
|
||||
)
|
||||
case "owncloud":
|
||||
h = handler.NewOwnCloudHandler(
|
||||
handler.Logger(s.log),
|
||||
handler.Config(s.c),
|
||||
)
|
||||
case "config":
|
||||
h = handler.NewConfigHandler(
|
||||
handler.Logger(s.log),
|
||||
handler.Config(s.c),
|
||||
handler.YubiAuth(s.yubiAuth),
|
||||
)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported backend %s - must be 'config', 'ldap' or 'owncloud'", s.c.Backend.Datastore)
|
||||
for i, backend := range s.c.Backends {
|
||||
var h handler.Handler
|
||||
switch backend.Datastore {
|
||||
case "ldap":
|
||||
h = handler.NewLdapHandler(
|
||||
handler.Backend(backend),
|
||||
handler.Handlers(allHandlers),
|
||||
handler.Logger(s.log),
|
||||
handler.Config(s.c),
|
||||
handler.Helper(helper),
|
||||
)
|
||||
case "owncloud":
|
||||
h = handler.NewOwnCloudHandler(
|
||||
handler.Backend(backend),
|
||||
handler.Logger(s.log),
|
||||
handler.Config(s.c),
|
||||
)
|
||||
case "config":
|
||||
h = handler.NewConfigHandler(
|
||||
handler.Backend(backend),
|
||||
handler.Logger(s.log),
|
||||
handler.Config(s.c),
|
||||
handler.YubiAuth(s.yubiAuth),
|
||||
)
|
||||
case "plugin":
|
||||
plug, err := plugin.Open(backend.Plugin)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Unable to load specified backend plugin: %s", err))
|
||||
}
|
||||
nph, err := plug.Lookup(backend.PluginHandler)
|
||||
if err != nil {
|
||||
return nil, errors.New("Unable to find 'NewPluginHandler' in loaded backend plugin")
|
||||
}
|
||||
initFunc, ok := nph.(func(...handler.Option) handler.Handler)
|
||||
|
||||
if !ok {
|
||||
return nil, errors.New("Loaded backend plugin lacks a proper NewPluginHandler function")
|
||||
}
|
||||
// Normally, here, we would somehow have imported our plugin into our
|
||||
// handler namespace. Oops?
|
||||
h = initFunc(
|
||||
handler.Backend(backend),
|
||||
handler.Logger(s.log),
|
||||
handler.Config(s.c),
|
||||
handler.YubiAuth(s.yubiAuth),
|
||||
)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported backend %s - must be one of 'config', 'ldap','owncloud' or 'plugin'", backend.Datastore)
|
||||
}
|
||||
s.log.V(3).Info("Loading backend", "datastore", backend.Datastore, "position", i)
|
||||
|
||||
// Only our first backend will answer proper LDAP queries.
|
||||
// Note that this could evolve towars something nicer where we would maintain
|
||||
// multiple binders in addition to the existing multiple LDAP backends
|
||||
if i == 0 {
|
||||
s.l.BindFunc("", h)
|
||||
s.l.SearchFunc("", h)
|
||||
s.l.CloseFunc("", h)
|
||||
}
|
||||
allHandlers.Handlers[i] = h
|
||||
backendCounter++
|
||||
}
|
||||
s.log.V(3).Info("Using backend", "datastore", s.c.Backend.Datastore)
|
||||
s.l.BindFunc("", h)
|
||||
s.l.SearchFunc("", h)
|
||||
s.l.CloseFunc("", h)
|
||||
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user