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:
Chris F Ravenscroft
2021-07-17 11:45:36 -07:00
committed by GitHub
parent 225f1b441b
commit 2eb2215ae4
19 changed files with 1519 additions and 90 deletions
+3 -1
View File
@@ -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
+38 -11
View File
@@ -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
}
+7 -1
View File
@@ -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
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+35
View File
@@ -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
View File
@@ -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,
},
},
},
+22
View File
@@ -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 $@ $^
+132
View File
@@ -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.
+597
View File
@@ -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() {}
+55
View File
@@ -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()
}
+55
View File
@@ -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()
}
+82
View File
@@ -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"
+45
View File
@@ -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"
+55
View File
@@ -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
View File
@@ -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
}