diff --git a/.gitignore b/.gitignore index 539c12b..67ea45a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ certs/ # Built binaries bin -bindata.go +pkg/assets/bindata.go .*.swp diff --git a/Dockerfile b/Dockerfile index 1528d8f..4f03d33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ RUN apk add --no-cache git bzr make RUN go get -d -v ./... # Run go-bindata to embed data for API -RUN go get -u github.com/jteeuwen/go-bindata/... && $GOPATH/bin/go-bindata -pkg=main assets && gofmt -w bindata.go +RUN go get -u github.com/jteeuwen/go-bindata/... && $GOPATH/bin/go-bindata -pkg=assets -o=pkg/assets/bindata.go assets && gofmt -w pkg/assets/bindata.go # Build and copy final result RUN make linux64 && cp ./bin/glauth64 /app/glauth diff --git a/Makefile b/Makefile index 17554f9..9582c67 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ GIT_BRANCH=$(shell git rev-parse --abbrev-ref HEAD) # Build variables BUILD_VARS=-X main.GitCommit=${GIT_COMMIT} -X main.GitBranch=${GIT_BRANCH} -X main.BuildTime=${BUILD_TIME} -X main.GitClean=${GIT_CLEAN} -X main.LastGitTag=${LAST_GIT_TAG} -X main.GitTagIsCommit=${GIT_IS_TAG_COMMIT} -BUILD_FILES=glauth.go bindata.go ldapbackend.go webapi.go configbackend.go owncloudbackend.go +BUILD_FILES=glauth.go TRIM_FLAGS=-gcflags "all=-trimpath=${PWD}" -asmflags "all=-trimpath=${PWD}" ##################### @@ -56,11 +56,11 @@ updatetest: ./scripts/travis/integration-test.sh bindata: - go get -u github.com/jteeuwen/go-bindata/... && ${GOPATH}/bin/go-bindata -pkg=main assets && gofmt -w bindata.go + go get -u github.com/jteeuwen/go-bindata/... && ${GOPATH}/bin/go-bindata -pkg=assets -o=pkg/assets/bindata.go assets && gofmt -w pkg/assets/bindata.go cleanup: - rm bindata.go + rm pkg/assets/bindata.go format: go fmt diff --git a/glauth.go b/glauth.go index 740333f..ea69389 100644 --- a/glauth.go +++ b/glauth.go @@ -1,7 +1,6 @@ package main import ( - "expvar" "fmt" "os" "strings" @@ -10,8 +9,11 @@ import ( "github.com/GeertJohan/yubigo" docopt "github.com/docopt/docopt-go" "github.com/fsnotify/fsnotify" + "github.com/glauth/glauth/pkg/config" + "github.com/glauth/glauth/pkg/frontend" + "github.com/glauth/glauth/pkg/server" + "github.com/glauth/glauth/pkg/stats" "github.com/jinzhu/copier" - "github.com/nmcclain/ldap" logging "github.com/op/go-logging" "gopkg.in/amz.v1/aws" "gopkg.in/amz.v1/s3" @@ -48,99 +50,10 @@ var ( args map[string]interface{} stderr *logging.LogBackend yubiAuth *yubigo.YubiAuth - // exposed expvar variables - // TODO: Should be renamed according to golang naming conventions for exported vars, StatsFrontend, StatsBackend, StatsGeneral - stats_frontend = expvar.NewMap("proxy_frontend") - stats_backend = expvar.NewMap("proxy_backend") - stats_general = expvar.NewMap("proxy") - activeConfig = &config{} + activeConfig = &config.Config{} ) -// interface for backend handler -type Backend interface { - ldap.Binder - ldap.Searcher - ldap.Closer -} - -// config file -type configBackend struct { - BaseDN string - Datastore string - Insecure bool // For LDAP backend only - Servers []string // For LDAP and ownCloud backend only - NameFormat string - GroupFormat string - SSHKeyAttr string - UseGraphAPI bool // For ownCloud backend only -} -type configFrontend struct { - AllowedBaseDNs []string // For LDAP backend only - Listen string - Cert string - Key string - TLS bool -} -type configLDAP struct { - Enabled bool - Listen string -} -type configLDAPS struct { - Enabled bool - Listen string - Cert string - Key string -} -type configAPI struct { - Cert string - Enabled bool - Key string - Listen string - SecretToken string - TLS bool -} -type configUser struct { - Name string - OtherGroups []int - PassSHA256 string - PassAppSHA256 []string - PrimaryGroup int - SSHKeys []string - OTPSecret string - Yubikey string - Disabled bool - UnixID int - Mail string - LoginShell string - GivenName string - SN string - Homedir string -} -type configGroup struct { - Name string - UnixID int - IncludeGroups []int -} -type config struct { - API configAPI - Backend configBackend - Debug bool - WatchConfig bool - YubikeyClientID string - YubikeySecret string - Frontend configFrontend - LDAP configLDAP - LDAPS configLDAPS - Groups []configGroup - Syslog bool - Users []configUser - ConfigFile string - AwsAccessKeyId string - AwsSecretAccessKey string - AwsRegion string -} - // Reads builtime vars and returns a full string containing info about // the currently running version of the software. Primarily used by the // --version flag at runtime. @@ -199,50 +112,21 @@ func main() { func startService() { // stats - stats_general.Set("version", stringer(LastGitTag)) + stats.General.Set("version", stats.Stringer(LastGitTag)) // web API if activeConfig.API.Enabled { log.Debug("Web API enabled") - go RunAPI(activeConfig) + go frontend.RunAPI(log, activeConfig) } startConfigWatcher() - // configure the backend - s := ldap.NewServer() - s.EnforceLDAP = true - var handler Backend - switch activeConfig.Backend.Datastore { - case "ldap": - handler = newLdapHandler(activeConfig) - case "owncloud": - handler = newOwnCloudHandler(activeConfig) - case "config": - handler = newConfigHandler(activeConfig, yubiAuth) - default: - log.Fatalf("Unsupported backend %s - must be 'config' or 'ldap'.", activeConfig.Backend.Datastore) - } - log.Notice(fmt.Sprintf("Using %s backend", activeConfig.Backend.Datastore)) - s.BindFunc("", handler) - s.SearchFunc("", handler) - s.CloseFunc("", handler) - - if activeConfig.LDAP.Enabled { - // Dont block if also starting a LDAPS server afterwards - shouldBlock := !activeConfig.LDAPS.Enabled - - if shouldBlock { - startLDAP(&activeConfig.LDAP, s) - } else { - go startLDAP(&activeConfig.LDAP, s) - } - } - - if activeConfig.LDAPS.Enabled { - // Always block here - startLDAPS(&activeConfig.LDAPS, s) + s, err := server.NewServer(log, activeConfig) + if err != nil { + log.Fatalf("Could not start server: %s", err.Error()) } + s.ListenAndServe() log.Critical("AP exit") } @@ -283,20 +167,6 @@ func startConfigWatcher() { watcher.Add(configFileLocation) } -func startLDAP(ldapConfig *configLDAP, server *ldap.Server) { - log.Notice(fmt.Sprintf("LDAP server listening on %s", ldapConfig.Listen)) - if err := server.ListenAndServe(ldapConfig.Listen); err != nil { - log.Fatalf("LDAP Server Failed: %s", err.Error()) - } -} - -func startLDAPS(ldapsConfig *configLDAPS, server *ldap.Server) { - log.Notice(fmt.Sprintf("LDAPS server listening on %s", ldapsConfig.Listen)) - if err := server.ListenAndServeTLS(ldapsConfig.Listen, ldapsConfig.Cert, ldapsConfig.Key); err != nil { - log.Fatalf("LDAP Server Failed: %s", err.Error()) - } -} - func parseArgs() error { var err error @@ -311,8 +181,8 @@ func getConfigLocation() string { return args["--config"].(string) } -func parseConfigFile(configFileLocation string) (*config, error) { - cfg := config{} +func parseConfigFile(configFileLocation string) (*config.Config, error) { + cfg := config.Config{} // setup defaults cfg.LDAP.Enabled = false cfg.LDAPS.Enabled = true @@ -359,7 +229,7 @@ func parseConfigFile(configFileLocation string) (*config, error) { return &cfg, nil } -func handleConfig(cfg config) (*config, error) { +func handleConfig(cfg config.Config) (*config.Config, error) { if len(cfg.Frontend.Listen) > 0 && (len(cfg.LDAP.Listen) > 0 || len(cfg.LDAPS.Listen) > 0) { // Both old server-config and new - dont allow return &cfg, fmt.Errorf("Both old and new server-config in use - please remove old format ([frontend]) and migrate to new format ([ldap], [ldaps])") diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5071c20 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module github.com/glauth/glauth + +go 1.13 + +require ( + github.com/BurntSushi/toml v0.3.1 + github.com/GeertJohan/yubigo v0.0.0-20190917122436-175bc097e60e + github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 + github.com/fsnotify/fsnotify v1.4.7 + github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a + github.com/kr/pretty v0.2.0 + github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 // indirect + github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3 + github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 + github.com/pquerna/otp v1.2.0 + github.com/yaegashi/msgraph.go v0.1.1-0.20200221123608-2d438cf2a7cc + golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c // indirect + gopkg.in/amz.v1 v1.0.0-20150111123259-ad23e96a31d2 + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c99c51e --- /dev/null +++ b/go.sum @@ -0,0 +1,44 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GeertJohan/yubigo v0.0.0-20190917122436-175bc097e60e h1:Bqtt5C+uVk+vH/t5dmB47uDCTwxw16EYHqvJnmY2aQc= +github.com/GeertJohan/yubigo v0.0.0-20190917122436-175bc097e60e/go.mod h1:njRCDrl+1RQ/A/+KVU8Ho2EWAxUSkohOWczdW3dzDG0= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/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/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.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +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/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/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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= +github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3/go.mod h1:YtrVB1/v9Td9SyjXpjYVmbdKgj9B0nPTBsdGUxy0i8U= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok= +github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/yaegashi/msgraph.go v0.1.1-0.20200221123608-2d438cf2a7cc h1:ejaC8rvIvCWmsaFrvmGOxhBuMxxhBB1xRshuM98XQ7M= +github.com/yaegashi/msgraph.go v0.1.1-0.20200221123608-2d438cf2a7cc/go.mod h1:tso14hwzqX4VbnWTNsxiL0DvMb2OwbGISFA7jDibdWc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ= +golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +gopkg.in/amz.v1 v1.0.0-20150111123259-ad23e96a31d2 h1:FMrsB0OTjHsPDA1NM7AhRmmZzkBPu3iGdxK/5MFfBmk= +gopkg.in/amz.v1 v1.0.0-20150111123259-ad23e96a31d2/go.mod h1:F0YaN4yi2XekmElKkPYfybh7pReQE9Ph48lSgeCkzyc= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..e23489e --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,78 @@ +package config + +// config file +type Backend struct { + BaseDN string + Datastore string + Insecure bool // For LDAP backend only + Servers []string // For LDAP backend only + NameFormat string + GroupFormat string + SSHKeyAttr string + UseGraphAPI bool // For ownCloud backend only +} +type Frontend struct { + AllowedBaseDNs []string // For LDAP backend only + Listen string + Cert string + Key string + TLS bool +} +type LDAP struct { + Enabled bool + Listen string +} +type LDAPS struct { + Enabled bool + Listen string + Cert string + Key string +} +type API struct { + Cert string + Enabled bool + Key string + Listen string + SecretToken string + TLS bool +} +type User struct { + Name string + OtherGroups []int + PassSHA256 string + PassAppSHA256 []string + PrimaryGroup int + SSHKeys []string + OTPSecret string + Yubikey string + Disabled bool + UnixID int + Mail string + LoginShell string + GivenName string + SN string + Homedir string +} +type Group struct { + Name string + UnixID int + IncludeGroups []int +} +type Config struct { + API API + Backend Backend + Debug bool + WatchConfig bool + YubikeyClientID string + YubikeySecret string + Frontend Frontend + LDAP LDAP + LDAPS LDAPS + Groups []Group + Syslog bool + Users []User + ConfigFile string + AwsAccessKeyId string + AwsSecretAccessKey string + AwsRegion string +} diff --git a/webapi.go b/pkg/frontend/webapi.go similarity index 53% rename from webapi.go rename to pkg/frontend/webapi.go index cbea63f..e90158a 100644 --- a/webapi.go +++ b/pkg/frontend/webapi.go @@ -1,4 +1,4 @@ -package main +package frontend import ( "bytes" @@ -6,10 +6,14 @@ import ( "io" "net/http" "strings" + + "github.com/glauth/glauth/pkg/assets" + "github.com/glauth/glauth/pkg/config" + logging "github.com/op/go-logging" ) // RunAPI provides a basic REST API -func RunAPI(cfg *config) { +func RunAPI(log *logging.Logger, cfg *config.Config) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { log.Debug(fmt.Sprintf("Web: %s", r.URL.Path)) if r.URL.Path != "/" { @@ -17,13 +21,13 @@ func RunAPI(cfg *config) { http.NotFound(w, r) return } - pageTemplate, err := Asset("assets/index.html") + pageTemplate, err := assets.Asset("assets/index.html") if err != nil { log.Fatal(fmt.Sprintf("Error with HTTP server template asset: %s", err.Error())) } fmt.Fprintf(w, string(pageTemplate)) }) - http.HandleFunc("/assets/", webStaticHandler) + http.HandleFunc("/assets/", webStaticHandler(log)) if cfg.API.TLS { log.Notice(fmt.Sprintf("Starting HTTPS server on %s", cfg.API.Listen)) err := http.ListenAndServeTLS(cfg.API.Listen, cfg.API.Cert, cfg.API.Key, nil) @@ -40,23 +44,21 @@ func RunAPI(cfg *config) { } // webStaticHandler serves embedded static web files (js&css) -func webStaticHandler(w http.ResponseWriter, r *http.Request) { - assetPath := r.URL.Path[1:] - staticAsset, err := Asset(assetPath) - if err != nil { - log.Error(err.Error()) - http.NotFound(w, r) - return +func webStaticHandler(log *logging.Logger) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + assetPath := r.URL.Path[1:] + staticAsset, err := assets.Asset(assetPath) + if err != nil { + log.Error(err.Error()) + http.NotFound(w, r) + return + } + headers := w.Header() + if strings.HasSuffix(assetPath, ".js") { + headers["Content-Type"] = []string{"application/javascript"} + } else if strings.HasSuffix(assetPath, ".css") { + headers["Content-Type"] = []string{"text/css"} + } + io.Copy(w, bytes.NewReader(staticAsset)) } - headers := w.Header() - if strings.HasSuffix(assetPath, ".js") { - headers["Content-Type"] = []string{"application/javascript"} - } else if strings.HasSuffix(assetPath, ".css") { - headers["Content-Type"] = []string{"text/css"} - } - io.Copy(w, bytes.NewReader(staticAsset)) } - -type stringer string - -func (s stringer) String() string { return string(s) } diff --git a/configbackend.go b/pkg/handler/config.go similarity index 85% rename from configbackend.go rename to pkg/handler/config.go index a76afc9..2d8af22 100644 --- a/configbackend.go +++ b/pkg/handler/config.go @@ -1,4 +1,4 @@ -package main +package handler import ( "crypto/sha256" @@ -9,17 +9,22 @@ import ( "strings" "github.com/GeertJohan/yubigo" + "github.com/glauth/glauth/pkg/config" + "github.com/glauth/glauth/pkg/stats" "github.com/nmcclain/ldap" + "github.com/op/go-logging" "github.com/pquerna/otp/totp" ) type configHandler struct { - cfg *config + log *logging.Logger + cfg *config.Config yubikeyAuth *yubigo.YubiAuth } -func newConfigHandler(cfg *config, yubikeyAuth *yubigo.YubiAuth) Backend { +func NewConfigHandler(log *logging.Logger, cfg *config.Config, yubikeyAuth *yubigo.YubiAuth) Handler { handler := configHandler{ + log: log, cfg: cfg, yubikeyAuth: yubikeyAuth} return handler @@ -30,14 +35,14 @@ func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultC bindDN = strings.ToLower(bindDN) baseDN := strings.ToLower("," + h.cfg.Backend.BaseDN) - log.Debug(fmt.Sprintf("Bind request: bindDN: %s, BaseDN: %s, source: %s", bindDN, h.cfg.Backend.BaseDN, conn.RemoteAddr().String())) + h.log.Debug(fmt.Sprintf("Bind request: bindDN: %s, BaseDN: %s, source: %s", bindDN, h.cfg.Backend.BaseDN, conn.RemoteAddr().String())) - stats_frontend.Add("bind_reqs", 1) + stats.Frontend.Add("bind_reqs", 1) // parse the bindDN - ensure that the bindDN ends with the BaseDN if !strings.HasSuffix(bindDN, baseDN) { - log.Warning(fmt.Sprintf("Bind Error: BindDN %s not our BaseDN %s", bindDN, h.cfg.Backend.BaseDN)) - // log.Warning(fmt.Sprintf("Bind Error: BindDN %s not our BaseDN %s", bindDN, baseDN)) + h.log.Warning(fmt.Sprintf("Bind Error: BindDN %s not our BaseDN %s", bindDN, h.cfg.Backend.BaseDN)) + // h.log.Warning(fmt.Sprintf("Bind Error: BindDN %s not our BaseDN %s", bindDN, baseDN)) return ldap.LDAPResultInvalidCredentials, nil } parts := strings.Split(strings.TrimSuffix(bindDN, baseDN), ",") @@ -49,12 +54,12 @@ func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultC userName = strings.TrimPrefix(parts[0], h.cfg.Backend.NameFormat+"=") groupName = strings.TrimPrefix(parts[1], h.cfg.Backend.GroupFormat+"=") } else { - log.Warning(fmt.Sprintf("Bind Error: BindDN %s should have only one or two parts (has %d)", bindDN, len(parts))) + h.log.Warning(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 := configUser{} + user := config.User{} found := false for _, u := range h.cfg.Users { if u.Name == userName { @@ -63,11 +68,11 @@ func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultC } } if !found { - log.Warning(fmt.Sprintf("Bind Error: User %s not found.", userName)) + h.log.Warning(fmt.Sprintf("Bind Error: User %s not found.", userName)) return ldap.LDAPResultInvalidCredentials, nil } // find the group - group := configGroup{} + group := config.Group{} found = false for _, g := range h.cfg.Groups { if g.Name == groupName { @@ -76,12 +81,12 @@ func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultC } } if !found { - log.Warning(fmt.Sprintf("Bind Error: Group %s not found.", groupName)) + h.log.Warning(fmt.Sprintf("Bind Error: Group %s not found.", groupName)) return ldap.LDAPResultInvalidCredentials, nil } // validate group membership if user.PrimaryGroup != group.UnixID { - log.Warning(fmt.Sprintf("Bind Error: User %s primary group is not %s.", userName, groupName)) + h.log.Warning(fmt.Sprintf("Bind Error: User %s primary group is not %s.", userName, groupName)) return ldap.LDAPResultInvalidCredentials, nil } @@ -129,10 +134,10 @@ func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultC for index, appPw := range user.PassAppSHA256 { if appPw != hex.EncodeToString(hashFull.Sum(nil)) { - log.Debug(fmt.Sprintf("Attempted to bind app pw #%d - failure as %s from %s", index, bindDN, conn.RemoteAddr().String())) + h.log.Debug(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) - log.Debug("Bind success using app pw #%d as %s from %s", index, bindDN, conn.RemoteAddr().String()) + stats.Frontend.Add("bind_successes", 1) + h.log.Debug("Bind success using app pw #%d as %s from %s", index, bindDN, conn.RemoteAddr().String()) return ldap.LDAPResultSuccess, nil } @@ -144,18 +149,18 @@ func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultC // Then ensure the OTP is valid before checking if !validotp { - log.Warning(fmt.Sprintf("Bind Error: invalid OTP token as %s from %s", bindDN, conn.RemoteAddr().String())) + h.log.Warning(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)) { - log.Warning(fmt.Sprintf("Bind Error: invalid credentials as %s from %s", bindDN, conn.RemoteAddr().String())) + h.log.Warning(fmt.Sprintf("Bind Error: invalid credentials as %s from %s", bindDN, conn.RemoteAddr().String())) return ldap.LDAPResultInvalidCredentials, nil } - stats_frontend.Add("bind_successes", 1) - log.Debug(fmt.Sprintf("Bind success as %s from %s", bindDN, conn.RemoteAddr().String())) + stats.Frontend.Add("bind_successes", 1) + h.log.Debug(fmt.Sprintf("Bind success as %s from %s", bindDN, conn.RemoteAddr().String())) return ldap.LDAPResultSuccess, nil } @@ -164,8 +169,8 @@ func (h configHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn bindDN = strings.ToLower(bindDN) baseDN := strings.ToLower("," + h.cfg.Backend.BaseDN) searchBaseDN := strings.ToLower(searchReq.BaseDN) - log.Debug(fmt.Sprintf("Search request as %s from %s for %s", bindDN, conn.RemoteAddr().String(), searchReq.Filter)) - stats_frontend.Add("search_reqs", 1) + h.log.Debug(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 { @@ -260,14 +265,14 @@ func (h configHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) } } - stats_frontend.Add("search_successes", 1) - log.Debug(fmt.Sprintf("AP: Search OK: %s", searchReq.Filter)) + stats.Frontend.Add("search_successes", 1) + h.log.Debug(fmt.Sprintf("AP: Search OK: %s", searchReq.Filter)) return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil } // func (h configHandler) Close(boundDn string, conn net.Conn) error { - stats_frontend.Add("closes", 1) + stats.Frontend.Add("closes", 1) return nil } @@ -331,7 +336,7 @@ func (h configHandler) getGroupMemberIDs(gid int) []string { if gid == g.UnixID { for _, includegroupid := range g.IncludeGroups { if includegroupid == gid { - log.Warning(fmt.Sprintf("Group: %d - Ignoring myself as included group", includegroupid)) + h.log.Warning(fmt.Sprintf("Group: %d - Ignoring myself as included group", includegroupid)) } else { includegroupmemberids := h.getGroupMemberIDs(includegroupid) diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go new file mode 100644 index 0000000..985f4be --- /dev/null +++ b/pkg/handler/handler.go @@ -0,0 +1,10 @@ +package handler + +import "github.com/nmcclain/ldap" + +// interface for backend handler +type Handler interface { + ldap.Binder + ldap.Searcher + ldap.Closer +} diff --git a/ldapbackend.go b/pkg/handler/ldap.go similarity index 73% rename from ldapbackend.go rename to pkg/handler/ldap.go index 6e00dd6..959f84a 100644 --- a/ldapbackend.go +++ b/pkg/handler/ldap.go @@ -1,4 +1,4 @@ -package main +package handler import ( "crypto/sha256" @@ -12,13 +12,17 @@ import ( "sync" "time" + "github.com/glauth/glauth/pkg/config" + "github.com/glauth/glauth/pkg/stats" "github.com/kr/pretty" "github.com/nmcclain/ldap" + "github.com/op/go-logging" ) type ldapHandler struct { doPing chan bool - cfg *config + log *logging.Logger + cfg *config.Config lock *sync.Mutex // for sessions and servers sessions map[string]ldapSession servers []ldapBackend @@ -47,10 +51,11 @@ type ldapBackend struct { Ping time.Duration } -func newLdapHandler(cfg *config) Backend { +func NewLdapHandler(log *logging.Logger, cfg *config.Config) Handler { handler := ldapHandler{ // set non-zero-value defaults here sessions: make(map[string]ldapSession), doPing: make(chan bool), + log: log, cfg: cfg, lock: &ldaplock, } @@ -71,31 +76,31 @@ func newLdapHandler(cfg *config) Backend { // func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCode ldap.LDAPResultCode, err error) { - log.Debug(fmt.Sprintf("Bind request as %s from %s", bindDN, conn.RemoteAddr().String())) - stats_frontend.Add("bind_reqs", 1) + h.log.Debug(fmt.Sprintf("Bind request as %s from %s", bindDN, conn.RemoteAddr().String())) + stats.Frontend.Add("bind_reqs", 1) s, err := h.getSession(conn) if err != nil { - stats_frontend.Add("bind_ldapSession_errors", 1) - log.Debug(fmt.Sprintf("Bind ops error as %s from %s == %s", bindDN, conn.RemoteAddr().String(), err.Error())) + stats.Frontend.Add("bind_ldapSession_errors", 1) + h.log.Debug(fmt.Sprintf("Bind ops error as %s from %s == %s", bindDN, conn.RemoteAddr().String(), err.Error())) return ldap.LDAPResultOperationsError, err } if err := s.ldap.Bind(bindDN, bindSimplePw); err != nil { - stats_frontend.Add("bind_errors", 1) - log.Debug(fmt.Sprintf("Bind invalid creds as %s from %s", bindDN, conn.RemoteAddr().String())) + stats.Frontend.Add("bind_errors", 1) + h.log.Debug(fmt.Sprintf("Bind invalid creds as %s from %s", bindDN, conn.RemoteAddr().String())) return ldap.LDAPResultInvalidCredentials, nil } - stats_frontend.Add("bind_successes", 1) - log.Debug(fmt.Sprintf("Bind success as %s from %s", bindDN, conn.RemoteAddr().String())) + stats.Frontend.Add("bind_successes", 1) + h.log.Debug(fmt.Sprintf("Bind success as %s from %s", bindDN, conn.RemoteAddr().String())) return ldap.LDAPResultSuccess, nil } // func (h ldapHandler) Search(boundDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) { - log.Debug(fmt.Sprintf("Search request as %s from %s for %s", boundDN, conn.RemoteAddr().String(), searchReq.Filter)) - stats_frontend.Add("search_reqs", 1) + h.log.Debug(fmt.Sprintf("Search request as %s from %s for %s", boundDN, conn.RemoteAddr().String(), searchReq.Filter)) + stats.Frontend.Add("search_reqs", 1) s, err := h.getSession(conn) if err != nil { - stats_frontend.Add("search_ldapSession_errors", 1) + stats.Frontend.Add("search_ldapSession_errors", 1) return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, nil } search := ldap.NewSearchRequest( @@ -110,24 +115,24 @@ func (h ldapHandler) Search(boundDN string, searchReq ldap.SearchRequest, conn n searchReq.Controls, ) - log.Debug(fmt.Sprintf("Search req to backend: %# v", pretty.Formatter(search))) + h.log.Debug(fmt.Sprintf("Search req to backend: %# v", pretty.Formatter(search))) sr, err := s.ldap.Search(search) - log.Debug(fmt.Sprintf("Backend Search result: %# v", pretty.Formatter(sr))) + h.log.Debug(fmt.Sprintf("Backend Search result: %# v", pretty.Formatter(sr))) ssr := ldap.ServerSearchResult{ Entries: sr.Entries, Referrals: sr.Referrals, Controls: sr.Controls, } - log.Debug(fmt.Sprintf("Frontend Search result: %# v", pretty.Formatter(ssr))) + h.log.Debug(fmt.Sprintf("Frontend Search result: %# v", pretty.Formatter(ssr))) if err != nil { e := err.(*ldap.Error) - log.Debug(fmt.Sprintf("Search Err: %# v", pretty.Formatter(err))) - stats_frontend.Add("search_errors", 1) + h.log.Debug(fmt.Sprintf("Search Err: %# v", pretty.Formatter(err))) + stats.Frontend.Add("search_errors", 1) ssr.ResultCode = ldap.LDAPResultCode(e.ResultCode) return ssr, err } - stats_frontend.Add("search_successes", 1) - log.Debug(fmt.Sprintf("AP: Search OK: %s -> num of entries = %d\n", search.Filter, len(ssr.Entries))) + stats.Frontend.Add("search_successes", 1) + h.log.Debug(fmt.Sprintf("AP: Search OK: %s -> num of entries = %d\n", search.Filter, len(ssr.Entries))) return ssr, nil } func (h ldapHandler) Close(boundDn string, conn net.Conn) error { @@ -135,8 +140,8 @@ func (h ldapHandler) Close(boundDn string, conn net.Conn) error { h.lock.Lock() defer h.lock.Unlock() delete(h.sessions, connID(conn)) - stats_frontend.Add("closes", 1) - stats_backend.Add("closes", 1) + stats.Frontend.Add("closes", 1) + stats.Backend.Add("closes", 1) return nil } @@ -144,22 +149,22 @@ func (h ldapHandler) Close(boundDn string, conn net.Conn) error { func (h *ldapHandler) monitorServers() { err := h.ping() if err != nil { - log.Fatal(err) + h.log.Fatal(err) } go func() { for { select { case <-h.doPing: - log.Notice("doPing requested due to server failure") + h.log.Notice("doPing requested due to server failure") err = h.ping() if err != nil { - log.Fatal(err) + h.log.Fatal(err) } case <-time.NewTimer(60 * time.Second).C: - log.Debug("doPing after timeout") + h.log.Debug("doPing after timeout") err = h.ping() if err != nil { - log.Fatal(err) + h.log.Fatal(err) } } } @@ -223,7 +228,7 @@ func (h ldapHandler) ping() error { elapsed := time.Since(start) h.lock.Lock() if err != nil || l == nil { - log.Error(fmt.Sprintf("Server %s:%d ping failed: %s", s.Hostname, s.Port, err.Error())) + h.log.Error(fmt.Sprintf("Server %s:%d ping failed: %s", s.Hostname, s.Port, err.Error())) h.servers[k].Ping = 0 h.servers[k].Status = Down } else { @@ -234,12 +239,12 @@ func (h ldapHandler) ping() error { } h.lock.Unlock() } - log.Debug(fmt.Sprintf("Server health: %# v", pretty.Formatter(h.servers))) + h.log.Debug(fmt.Sprintf("Server health: %# v", pretty.Formatter(h.servers))) b, err := json.Marshal(h.servers) if err != nil { - log.Error(fmt.Sprintf("Error encoding tail data: %s", err.Error())) + h.log.Error(fmt.Sprintf("Error encoding tail data: %s", err.Error())) } - stats_backend.Set("servers", stringer(string(b))) + stats.Backend.Set("servers", stats.Stringer(string(b))) if healthy == false { return fmt.Errorf("No healthy servers") } @@ -263,7 +268,7 @@ func (h ldapHandler) getBestServer() (ldapBackend, error) { if bestping == forever { return ldapBackend{}, fmt.Errorf("No healthy servers found") } - log.Debug(fmt.Sprintf("Best server: %# v", pretty.Formatter(favorite))) + h.log.Debug(fmt.Sprintf("Best server: %# v", pretty.Formatter(favorite))) return favorite, nil } diff --git a/owncloudbackend.go b/pkg/handler/owncloud.go similarity index 69% rename from owncloudbackend.go rename to pkg/handler/owncloud.go index 6f2b51d..7e90df5 100644 --- a/owncloudbackend.go +++ b/pkg/handler/owncloud.go @@ -1,89 +1,96 @@ -package main +package handler import ( "context" "encoding/json" "errors" "fmt" - msgraph "github.com/DeepDiver1975/msgraph.go/v1.0" - "github.com/nmcclain/ldap" "io/ioutil" "net" "net/http" "strings" "sync" + + "github.com/glauth/glauth/pkg/config" + "github.com/glauth/glauth/pkg/stats" + "github.com/nmcclain/ldap" + "github.com/op/go-logging" + msgraph "github.com/yaegashi/msgraph.go/v1.0" ) type ownCloudSession struct { + log *logging.Logger user string password string baseUrl string useGraphAPI bool } type ownCloudHandler struct { - cfg *config + log *logging.Logger + cfg *config.Config meUrl string sessions map[string]ownCloudSession lock sync.Mutex } -func (o ownCloudHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) { +func (h ownCloudHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) { bindDN = strings.ToLower(bindDN) - baseDN := strings.ToLower("," + o.cfg.Backend.BaseDN) + baseDN := strings.ToLower("," + h.cfg.Backend.BaseDN) - log.Debug(fmt.Sprintf("Bind request: bindDN: %s, BaseDN: %s, source: %s", bindDN, o.cfg.Backend.BaseDN, conn.RemoteAddr().String())) + h.log.Debug(fmt.Sprintf("Bind request: bindDN: %s, BaseDN: %s, source: %s", bindDN, h.cfg.Backend.BaseDN, conn.RemoteAddr().String())) - stats_frontend.Add("bind_reqs", 1) + stats.Frontend.Add("bind_reqs", 1) // parse the bindDN - ensure that the bindDN ends with the BaseDN if !strings.HasSuffix(bindDN, baseDN) { - log.Warning(fmt.Sprintf("Bind Error: BindDN %s not our BaseDN %s", bindDN, o.cfg.Backend.BaseDN)) + h.log.Warning(fmt.Sprintf("Bind Error: BindDN %s not our BaseDN %s", bindDN, h.cfg.Backend.BaseDN)) return ldap.LDAPResultInvalidCredentials, nil } parts := strings.Split(strings.TrimSuffix(bindDN, baseDN), ",") if len(parts) > 2 { - log.Warning(fmt.Sprintf("Bind Error: BindDN %s should have only one or two parts (has %d)", bindDN, len(parts))) + h.log.Warning(fmt.Sprintf("Bind Error: BindDN %s should have only one or two parts (has %d)", bindDN, len(parts))) return ldap.LDAPResultInvalidCredentials, nil } userName := strings.TrimPrefix(parts[0], "cn=") // try to login - if !o.login(userName, bindSimplePw) { - log.Warning(fmt.Sprintf("Bind Error: User %s login failed", userName)) + if !h.login(userName, bindSimplePw) { + h.log.Warning(fmt.Sprintf("Bind Error: User %s login failed", userName)) return ldap.LDAPResultInvalidCredentials, nil } id := connID(conn) - o.lock.Lock() - o.sessions[id] = ownCloudSession{ + h.lock.Lock() + h.sessions[id] = ownCloudSession{ + log: h.log, user: userName, password: bindSimplePw, - baseUrl: o.cfg.Backend.Servers[0], - useGraphAPI: o.cfg.Backend.UseGraphAPI, + baseUrl: h.cfg.Backend.Servers[0], + useGraphAPI: h.cfg.Backend.UseGraphAPI, } - o.lock.Unlock() + h.lock.Unlock() - stats_frontend.Add("bind_successes", 1) - log.Debug(fmt.Sprintf("Bind success as %s from %s", bindDN, conn.RemoteAddr().String())) + stats.Frontend.Add("bind_successes", 1) + h.log.Debug(fmt.Sprintf("Bind success as %s from %s", bindDN, conn.RemoteAddr().String())) return ldap.LDAPResultSuccess, nil } -func (o ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { +func (h ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { bindDN = strings.ToLower(bindDN) - baseDN := strings.ToLower("," + o.cfg.Backend.BaseDN) + baseDN := strings.ToLower("," + h.cfg.Backend.BaseDN) searchBaseDN := strings.ToLower(searchReq.BaseDN) - log.Debug(fmt.Sprintf("Search request as %s from %s for %s on %s", bindDN, conn.RemoteAddr().String(), searchReq.Filter, searchBaseDN)) - stats_frontend.Add("search_reqs", 1) + h.log.Debug(fmt.Sprintf("Search request as %s from %s for %s on %s", bindDN, conn.RemoteAddr().String(), searchReq.Filter, searchBaseDN)) + 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, o.cfg.Backend.BaseDN) + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("search error: BindDN %s not in our BaseDN %s", bindDN, h.cfg.Backend.BaseDN) } - if !strings.HasSuffix(searchBaseDN, o.cfg.Backend.BaseDN) { - return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("search error: search BaseDN %s is not in our BaseDN %s", searchBaseDN, o.cfg.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) } // return all users in the config file - the LDAP library will filter results for us entries := []*ldap.Entry{} @@ -91,10 +98,10 @@ func (o ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, con if err != nil { return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("search error: error parsing filter: %s", searchReq.Filter) } - o.lock.Lock() + h.lock.Lock() id := connID(conn) - session := o.sessions[id] - o.lock.Unlock() + session := h.sessions[id] + h.lock.Unlock() switch filterEntity { default: @@ -118,12 +125,12 @@ func (o 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, o.cfg.Backend.GroupFormat, o.cfg.Backend.BaseDN) + dn := fmt.Sprintf("cn=%s,%s=groups,%s", *g.ID, h.cfg.Backend.GroupFormat, h.cfg.Backend.BaseDN) entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) } case "posixaccount", "": userName := "" - if searchBaseDN != strings.ToLower(o.cfg.Backend.BaseDN) { + if searchBaseDN != strings.ToLower(h.cfg.Backend.BaseDN) { parts := strings.Split(strings.TrimSuffix(searchBaseDN, baseDN), ",") if len(parts) == 1 { userName = strings.TrimPrefix(parts[0], "cn=") @@ -131,7 +138,7 @@ func (o ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, con } users, err := session.getUsers(userName) if err != nil { - log.Debug(err) + h.log.Debug(err) return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("search error: error getting users") } for _, u := range users { @@ -148,27 +155,27 @@ func (o 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", o.cfg.Backend.NameFormat, *u.ID, o.cfg.Backend.GroupFormat, "users", o.cfg.Backend.BaseDN) + dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.cfg.Backend.NameFormat, *u.ID, h.cfg.Backend.GroupFormat, "users", h.cfg.Backend.BaseDN) entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) } } - stats_frontend.Add("search_successes", 1) - log.Debug(fmt.Sprintf("AP: Search OK: %s", searchReq.Filter)) + stats.Frontend.Add("search_successes", 1) + h.log.Debug(fmt.Sprintf("AP: Search OK: %s", searchReq.Filter)) return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil } -func (o ownCloudHandler) Close(boundDN string, conn net.Conn) error { +func (h ownCloudHandler) Close(boundDN string, conn net.Conn) error { conn.Close() // close connection to the server when then client is closed - o.lock.Lock() - defer o.lock.Unlock() - delete(o.sessions, connID(conn)) - stats_frontend.Add("closes", 1) - stats_backend.Add("closes", 1) + h.lock.Lock() + defer h.lock.Unlock() + delete(h.sessions, connID(conn)) + stats.Frontend.Add("closes", 1) + stats.Backend.Add("closes", 1) return nil } -func (o ownCloudHandler) login(name, pw string) bool { - req, _ := http.NewRequest("GET", o.meUrl, nil) +func (h ownCloudHandler) login(name, pw string) bool { + req, _ := http.NewRequest("GET", h.meUrl, nil) req.SetBasicAuth(name, pw) resp, err := http.DefaultClient.Do(req) if err != nil { @@ -191,22 +198,22 @@ type OCSGroupsResponse struct { } `json:"ocs"` } -func (o ownCloudSession) RoundTrip(req *http.Request) (*http.Response, error) { - req.SetBasicAuth(o.user, o.password) +func (s ownCloudSession) RoundTrip(req *http.Request) (*http.Response, error) { + req.SetBasicAuth(s.user, s.password) return http.DefaultTransport.RoundTrip(req) } -func (o ownCloudSession) getGroups() ([]msgraph.Group, error) { - if o.useGraphAPI { +func (s ownCloudSession) getGroups() ([]msgraph.Group, error) { + if s.useGraphAPI { ctx := context.Background() - req := o.NewClient().Groups().Request() + req := s.NewClient().Groups().Request() req.Expand("members") return req.Get(ctx) } - groupsUrl := fmt.Sprintf("%s/ocs/v2.php/cloud/groups?format=json", o.baseUrl) + groupsUrl := fmt.Sprintf("%s/ocs/v2.php/cloud/groups?format=json", s.baseUrl) req, _ := http.NewRequest("GET", groupsUrl, nil) - req.SetBasicAuth(o.user, o.password) + req.SetBasicAuth(s.user, s.password) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err @@ -243,21 +250,21 @@ type OCSUsersResponse struct { } // NewClient returns GraphService request builder with default base URL -func (o ownCloudSession) NewClient() *msgraph.GraphServiceRequestBuilder { - graphAPIBaseUrl := fmt.Sprintf("%s/index.php/apps/graphapi/v1.0/", o.baseUrl) +func (s ownCloudSession) NewClient() *msgraph.GraphServiceRequestBuilder { + graphAPIBaseUrl := fmt.Sprintf("%s/index.php/apps/graphapi/v1.0/", s.baseUrl) httpClient := &http.Client{ - Transport: o, + Transport: s, } g := msgraph.NewClient(httpClient) g.SetURL(graphAPIBaseUrl) return g } -func (o ownCloudSession) getUsers(userName string) ([]msgraph.User, error) { - if o.useGraphAPI { +func (s ownCloudSession) getUsers(userName string) ([]msgraph.User, error) { + if s.useGraphAPI { ctx := context.Background() - req := o.NewClient().Users() + req := s.NewClient().Users() if len(userName) > 0 { u, err := req.ID(userName).Request().Get(ctx) if err != nil { @@ -267,10 +274,10 @@ func (o ownCloudSession) getUsers(userName string) ([]msgraph.User, error) { } return req.Request().Get(ctx) } - usersUrl := fmt.Sprintf("%s/ocs/v2.php/cloud/users?format=json", o.baseUrl) + usersUrl := fmt.Sprintf("%s/ocs/v2.php/cloud/users?format=json", s.baseUrl) req, _ := http.NewRequest("GET", usersUrl, nil) - req.SetBasicAuth(o.user, o.password) + req.SetBasicAuth(s.user, s.password) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err @@ -293,16 +300,19 @@ func (o ownCloudSession) getUsers(userName string) ([]msgraph.User, error) { return ret, nil } -func (o ownCloudSession) redirectPolicyFunc(req *http.Request, via []*http.Request) error { - log.Debug("Setting user and password") - req.SetBasicAuth(o.user, o.password) +func (s ownCloudSession) redirectPolicyFunc(req *http.Request, via []*http.Request) error { + s.log.Debug("Setting user and password") + req.SetBasicAuth(s.user, s.password) return nil } -func newOwnCloudHandler(cfg *config) Backend { +func NewOwnCloudHandler(log *logging.Logger, cfg *config.Config) Handler { meUrl := fmt.Sprintf("%s/ocs/v2.php/cloud/user?format=json", cfg.Backend.Servers[0]) - handler := ownCloudHandler{cfg: cfg, meUrl: meUrl, + handler := ownCloudHandler{ + log: log, + cfg: cfg, + meUrl: meUrl, sessions: make(map[string]ownCloudSession), } return handler diff --git a/pkg/server/server.go b/pkg/server/server.go new file mode 100644 index 0000000..70e9833 --- /dev/null +++ b/pkg/server/server.go @@ -0,0 +1,91 @@ +package server + +import ( + "errors" + "fmt" + + "github.com/GeertJohan/yubigo" + "github.com/glauth/glauth/pkg/config" + "github.com/glauth/glauth/pkg/handler" + "github.com/nmcclain/ldap" + "github.com/op/go-logging" +) + +type LdapSvc struct { + log *logging.Logger + c *config.Config + yubiAuth *yubigo.YubiAuth + l *ldap.Server +} + +func NewServer(log *logging.Logger, cfg *config.Config) (*LdapSvc, error) { + + s := LdapSvc{ + log: log, + c: cfg, + } + + var err error + + if len(cfg.YubikeyClientID) > 0 && len(cfg.YubikeySecret) > 0 { + s.yubiAuth, err = yubigo.NewYubiAuth(cfg.YubikeyClientID, cfg.YubikeySecret) + + if err != nil { + return nil, errors.New("Yubikey Auth failed") + } + } + + // configure the backend + s.l = ldap.NewServer() + s.l.EnforceLDAP = true + var h handler.Handler + switch cfg.Backend.Datastore { + case "ldap": + h = handler.NewLdapHandler(log, cfg) + case "owncloud": + h = handler.NewOwnCloudHandler(log, cfg) + case "config": + h = handler.NewConfigHandler(log, cfg, s.yubiAuth) + default: + return nil, fmt.Errorf("unsupported backend %s - must be 'config' or 'ldap'", cfg.Backend.Datastore) + } + log.Notice(fmt.Sprintf("Using %s backend", cfg.Backend.Datastore)) + s.l.BindFunc("", h) + s.l.SearchFunc("", h) + s.l.CloseFunc("", h) + + return &s, nil +} + +func (s *LdapSvc) ListenAndServe() { + + if s.c.LDAP.Enabled { + // Dont block if also starting a LDAPS server afterwards + shouldBlock := !s.c.LDAPS.Enabled + + if shouldBlock { + s.startLDAP() + } else { + go s.startLDAP() + } + } + + if s.c.LDAPS.Enabled { + // Always block here + s.startLDAPS() + } +} + +func (s *LdapSvc) startLDAP() { + s.log.Notice(fmt.Sprintf("LDAP server listening on %s", s.c.LDAP.Listen)) + if err := s.l.ListenAndServe(s.c.LDAP.Listen); err != nil { + s.log.Fatalf("LDAP Server Failed: %s", err.Error()) + } +} + +func (s *LdapSvc) startLDAPS() { + s.log.Notice(fmt.Sprintf("LDAPS server listening on %s", s.c.LDAPS.Listen)) + if err := s.l.ListenAndServeTLS(s.c.LDAPS.Listen, s.c.LDAPS.Cert, s.c.LDAPS.Key); err != nil { + s.log.Fatalf("LDAP Server Failed: %s", err.Error()) + } +} diff --git a/pkg/stats/stats.go b/pkg/stats/stats.go new file mode 100644 index 0000000..bc34b33 --- /dev/null +++ b/pkg/stats/stats.go @@ -0,0 +1,10 @@ +package stats + +import "expvar" + +// exposed expvar variables +var ( + Frontend = expvar.NewMap("proxy_frontend") + Backend = expvar.NewMap("proxy_backend") + General = expvar.NewMap("proxy") +) diff --git a/pkg/stats/stringer.go b/pkg/stats/stringer.go new file mode 100644 index 0000000..a9a8530 --- /dev/null +++ b/pkg/stats/stringer.go @@ -0,0 +1,5 @@ +package stats + +type Stringer string + +func (s Stringer) String() string { return string(s) } diff --git a/sample-owncloud.cfg b/sample-owncloud.cfg new file mode 100644 index 0000000..6f91b11 --- /dev/null +++ b/sample-owncloud.cfg @@ -0,0 +1,22 @@ +################# +# General configuration. +debug = true + +################# +# Server configuration. + +# This ([ldap] and [ldaps]) is the new server-config format +[ldap] + enabled = true + listen = "localhost:3893" + +[ldaps] + enabled = false + +################# +# The backend section controls the data store. +[backend] + datastore = "owncloud" + baseDN = "dc=owncloud,dc=com" + servers = ["https://cloud.example.com"] + UseGraphAPI = true \ No newline at end of file