From 558c7de15a8bf6791eeca9145b3d28166d6cd83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 21 Feb 2020 17:10:41 +0100 Subject: [PATCH] Refactor packages (#105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Pulling in latest changes and fixes for 1.1.1 (#56) * Adding a few test dockerfiles * Continued work on docker builds * Basic example of docker build and run works - see build.sh for now * Continuing to work on docker build * Removing testing from build script, since it belongs later in process * Implementing dumb init * Docker build fully switched to alpine * Setting up alpine build * Cleaning up dockerfile * Cleaning up repo in prep for merge * Updating permissions for clean merge * Removing old ansible cruft, as per #12 * Removing binaries (since we now build) * Updating sample config to use new fields now available * Testing travis * another travis test * Still working on travis builds * Fix missing amazon packages * Merge in Travis config feature branch (#26) * Adding version string * Fixing and cleaning up docker build * Adding image hashing and verification to 'make all' * Testing build * Running gofmt to fix build * Testing switching travis to make * Testing build again * Merging in Fixes from travis-build feature branch (#33) This includes an integration test which runs glauth and compares ldapsearch snapshot output stored in the repo compared to the snapshot output of the glauth. Additionally, removing old versions of go, and adding windows and linuxar builds (but not tests) to the makefile, and consequently, the travis build. While the mac, linux-arm, and windows binaries are not able to be run in travis, they are able to be at least compiled. * Remove needless comment from integration test * Adding 'is-process-running' check before integration tests run, to clearly note if the program crashes on run * Switching from 1 sec to 2 sec timeout for integration test * Adding back config file to fix build. Previous commit intended to see if build failed (and it did) * Add support for including groups in groups (#23) * Add support for including groups in groups * Pulling in Makefile structure to allow easier testing and builds * Forgot to remove spare test * Add Version Info at Buildtime (#39) Adds the build info to the binary at buildtime via buildtime variables. This is shown by `./glauth --version`. Example output for a non-release: ``` GLauth Non-release build from branch feature/buildversion Build time: 20180531_181011Z Commit: 07ba631c2fd0050f375655ad7c44f862e0e6640c ``` And example of a built release: ``` GLauth v2.3.4 Build time: 20180601_041330Z Commit: 931d666262b96bab8a5c760be42d7facec7a2d74 ``` * Fixing Broken Docker Build (#41) Forgot to include go-bindata run in Dockerfile. * Removing leftover TODO from merge * Testing releases * Testing releases * Testing releases * Adjusting logging * Add new group tests to integration tests * add cleanup to test process * Add documentation about otherGroups, which was a previously undocumented field. * Auto fetching bindata during build so it's not needed to be done manually * Syntax fix * starting framework for unit tests * Merging in ongoing progress from Feature/travis build (#44) Fixes a few minor issues: #42 - now uses the makefile in the docker build, which means version info is correctly embedded at runtime - also now outputs version data at the top of the log when the container starts Fixing a simple issue found by go vet. Need to do more work on fixing issues found in go vet. * Forgot to run 'go fmt' * Adding codecov for go now that a single test is written * Testing not quite ready yet, removing from build * Add Support For 2 Factor Authentication Merging in feature from @ryskov (PR #24) adding 2FA support during LDAP binds. This is accomplished by concatenating the code to the end of the password. Also added integration tests for the TOTP method to run in CI. Could not, however, add automated tests for the yubikey, due the physical nature. * Expose LDAPS ports in Docker container (#49) Currently, the LDAPS ports are not exposed in the docker container. * Adding better logging to docker start script * Add more logging info to docker startup script * Fix Arm32 Build (#52) As discussed in issue #51, Arm32 builds were using 368 (intel/amd) arch accidentally, generating linux 32 bit binaries instead of arm 32 bit binaries. This commit fixes this in the Makefile, which will fix both the local builds, as well as the travis CI and release builds. fixes #52 * Update docker hub image badge to a working one previous one simply returned 0. * Removing 3893 - fixes #50 * Fixing readme MD formatting * Adding travis_retry to fix against intermittent network outages * Add TLS options for running both with TLS and without on the same time (#27) * Add TLS options for running both with TLS and without on the same time. This commit expands on the settings available for using TLS. It puts TLS settings under the [frontend.tls] section and adds a new setting to [frontend] called TLSExclusive (bool). TLSExclusive specifies whether or not to only run TLS when it is enabled, and is 'true' by default. Setting it to 'false' and having TLS enabled, causes the server to start both a LDAP and LDAPS server, and therefore requires to seperate 'listen' options (to run on different ports) - the Frontend.Listen and the Frontend.TLS.Listen. If TLSExclusive is set to 'true' and no Frontend.TLS.Listen is specified, it will use the Frontend.Listen. * Adding PR template and improving integration test tooling * Updating formatting * Add get dependencies step to makefile setup * Add go 1.11 * Add App Password Support (#60) App passwords can now be used to allow easier OTP use alongside applications which need to bind with a static password. Use the key `passappsha256` and specify an array of password hashes. See the readme and sample configuration file for more information. Fixes #54 * Adding NCoC as official project code of conduct We happily accept contributions based on the merit of the contributions. * Properly handling paramaters in logs - fixes #64 * Adding ldapsearch for healthchecks * fixed quoting in docker startup script (#77) Thanks @cxcv cxcv * Minor tweak of help text * More durable Dockerfile (#92) * refactor with latest dev Signed-off-by: Jörn Friedrich Dreyer * fix modules, update .gitignore * pass in logger Signed-off-by: Jörn Friedrich Dreyer Co-authored-by: Ben Yanke Co-authored-by: Andrea Cervesato --- .gitignore | 2 +- Dockerfile | 2 +- Makefile | 6 +- glauth.go | 158 ++---------------- go.mod | 20 +++ go.sum | 44 +++++ pkg/config/config.go | 78 +++++++++ webapi.go => pkg/frontend/webapi.go | 46 ++--- configbackend.go => pkg/handler/config.go | 57 ++++--- pkg/handler/handler.go | 10 ++ ldapbackend.go => pkg/handler/ldap.go | 71 ++++---- owncloudbackend.go => pkg/handler/owncloud.go | 136 ++++++++------- pkg/server/server.go | 91 ++++++++++ pkg/stats/stats.go | 10 ++ pkg/stats/stringer.go | 5 + sample-owncloud.cfg | 22 +++ 16 files changed, 465 insertions(+), 293 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/config/config.go rename webapi.go => pkg/frontend/webapi.go (53%) rename configbackend.go => pkg/handler/config.go (85%) create mode 100644 pkg/handler/handler.go rename ldapbackend.go => pkg/handler/ldap.go (73%) rename owncloudbackend.go => pkg/handler/owncloud.go (69%) create mode 100644 pkg/server/server.go create mode 100644 pkg/stats/stats.go create mode 100644 pkg/stats/stringer.go create mode 100644 sample-owncloud.cfg 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