From 91feda647189a9a8cfc795e5dec0fc68d184e678 Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 13 May 2019 09:19:33 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + Makefile | 40 ++++ cmd/server/config/config.go | 68 ++++++ cmd/server/config/provider.go | 9 + cmd/server/config/service.go | 30 +++ cmd/server/container.go | 44 ++++ cmd/server/main.go | 82 +++++++ cmd/server/public/css/common.css | 3 + cmd/server/public/css/login.css | 17 ++ cmd/server/public/css/profile.css | 0 cmd/server/public/img/logo.svg | 78 +++++++ cmd/server/route.go | 212 ++++++++++++++++++ cmd/server/templates/blocks/base.html.tmpl | 19 ++ cmd/server/templates/blocks/flash.html.tmpl | 23 ++ cmd/server/templates/layouts/login.html.tmpl | 41 ++++ .../templates/layouts/profile.html.tmpl | 46 ++++ go.mod | 16 ++ go.sum | 38 ++++ ldap/provider.go | 12 + ldap/search.go | 65 ++++++ ldap/service.go | 91 ++++++++ modd.conf | 9 + 22 files changed, 946 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cmd/server/config/config.go create mode 100644 cmd/server/config/provider.go create mode 100644 cmd/server/config/service.go create mode 100644 cmd/server/container.go create mode 100644 cmd/server/main.go create mode 100644 cmd/server/public/css/common.css create mode 100644 cmd/server/public/css/login.css create mode 100644 cmd/server/public/css/profile.css create mode 100644 cmd/server/public/img/logo.svg create mode 100644 cmd/server/route.go create mode 100644 cmd/server/templates/blocks/base.html.tmpl create mode 100644 cmd/server/templates/blocks/flash.html.tmpl create mode 100644 cmd/server/templates/layouts/login.html.tmpl create mode 100644 cmd/server/templates/layouts/profile.html.tmpl create mode 100644 go.mod create mode 100644 go.sum create mode 100644 ldap/provider.go create mode 100644 ldap/search.go create mode 100644 ldap/service.go create mode 100644 modd.conf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4caa611 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/bin +/data +/vendor \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3f6372c --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +export PATH := $(PATH):/usr/local/go/bin + +watch: + modd + +deps: vendor + GO111MODULE=off go get -u golang.org/x/tools/cmd/godoc + GO111MODULE=off go get -u github.com/cortesi/modd/cmd/modd + GO111MODULE=off go get -u github.com/golangci/golangci-lint/cmd/golangci-lint + $(FRONTEND_PACKAGE_MANAGER) install --verbose + +build: vendor bin/server + +release: vendor static doc + script/release + +test: + GO111MODULE=off go clean -testcache + go test -race -v -mod=vendor -cover -tags=test $(TEST_ARGS) ./... + +tidy: + go mod tidy + +vendor: tidy + go mod vendor + +bin/server: generate + CGO_ENABLED=0 go build -mod=vendor -v -o bin/server ./cmd/server + +lint: + GO111MODULE=off golangci-lint run --enable-all --disable gochecknoglobals + +clean: + rm -rf ./bin ./release ./vendor ./data + +godoc: + @echo "open your browser to http://localhost:6060/pkg/forge.cadoles.com/Cadoles/ldap-profile to see the documentation" + godoc -http=:6060 + +.PHONY: test run clean generate vendor deps lint tidy release \ No newline at end of file diff --git a/cmd/server/config/config.go b/cmd/server/config/config.go new file mode 100644 index 0000000..756966d --- /dev/null +++ b/cmd/server/config/config.go @@ -0,0 +1,68 @@ +package config + +import ( + "io" + + ini "gopkg.in/ini.v1" +) + +type Config struct { + HTTP HTTPConfig + LDAP LDAPConfig +} + +type HTTPConfig struct { + Address string + TemplateDir string + PublicDir string +} + +type LDAPConfig struct { + URL string + BaseDN string + UserSearchFilterPattern string + EditableAttributes []string `ini:",allowshadow"` +} + +// NewFromFile retrieves the configuration from the given file +func NewFromFile(filepath string) (*Config, error) { + config := NewDefault() + cfg, err := ini.Load(filepath) + if err != nil { + return nil, err + } + if err := cfg.MapTo(config); err != nil { + return nil, err + } + return config, nil +} + +func NewDefault() *Config { + return &Config{ + HTTP: HTTPConfig{ + Address: ":3000", + TemplateDir: "./templates", + PublicDir: "./public", + }, + LDAP: LDAPConfig{ + URL: "ldap://127.0.0.1:389", + BaseDN: "o=org,c=fr", + UserSearchFilterPattern: "(&(objectClass=person)(uid=%s))", + EditableAttributes: []string{ + "displayname", + "mail", + }, + }, + } +} + +func Dump(config *Config, w io.Writer) error { + cfg := ini.Empty() + if err := cfg.ReflectFrom(config); err != nil { + return err + } + if _, err := cfg.WriteTo(w); err != nil { + return err + } + return nil +} diff --git a/cmd/server/config/provider.go b/cmd/server/config/provider.go new file mode 100644 index 0000000..880162d --- /dev/null +++ b/cmd/server/config/provider.go @@ -0,0 +1,9 @@ +package config + +import "forge.cadoles.com/wpetit/goweb/service" + +func ServiceProvider(config *Config) service.Provider { + return func(ctn *service.Container) (interface{}, error) { + return config, nil + } +} diff --git a/cmd/server/config/service.go b/cmd/server/config/service.go new file mode 100644 index 0000000..6ef233d --- /dev/null +++ b/cmd/server/config/service.go @@ -0,0 +1,30 @@ +package config + +import ( + "forge.cadoles.com/wpetit/goweb/service" + "github.com/pkg/errors" +) + +const ServiceName service.Name = "config" + +// From retrieves the config service in the given container +func From(container *service.Container) (*Config, error) { + service, err := container.Service(ServiceName) + if err != nil { + return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName) + } + srv, ok := service.(*Config) + if !ok { + return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName) + } + return srv, nil +} + +// Must retrieves the config service in the given container or panic otherwise +func Must(container *service.Container) *Config { + srv, err := From(container) + if err != nil { + panic(err) + } + return srv +} diff --git a/cmd/server/container.go b/cmd/server/container.go new file mode 100644 index 0000000..195ec52 --- /dev/null +++ b/cmd/server/container.go @@ -0,0 +1,44 @@ +package main + +import ( + "forge.cadoles.com/Cadoles/ldap-profile/cmd/server/config" + "forge.cadoles.com/Cadoles/ldap-profile/ldap" + "forge.cadoles.com/wpetit/goweb/service" + "forge.cadoles.com/wpetit/goweb/service/session" + "forge.cadoles.com/wpetit/goweb/service/template" + "forge.cadoles.com/wpetit/goweb/session/gorilla" + "forge.cadoles.com/wpetit/goweb/template/html" + "github.com/gorilla/sessions" + "github.com/pkg/errors" +) + +func getServiceContainer(conf *config.Config) (*service.Container, error) { + + // Initialize and configure service container + ctn := service.NewContainer() + + // Create and initialize HTTP session service provider + cookieStore, err := gorilla.CreateCookieSessionStore(32, 64, &sessions.Options{ + Path: "/", + HttpOnly: true, + }) + if err != nil { + return nil, errors.Wrap(err, "error while creating session store") + } + ctn.Provide( + session.ServiceName, + gorilla.ServiceProvider("ldap-profile", cookieStore), + ) + + // Create and expose template service provider + ctn.Provide(template.ServiceName, html.ServiceProvider(conf.HTTP.TemplateDir)) + + // Create and expose ldap service provider + ctn.Provide(ldap.ServiceName, ldap.ServiceProvider(conf.LDAP.URL)) + + // Create and expose ldap service provider + ctn.Provide(config.ServiceName, config.ServiceProvider(conf)) + + return ctn, nil + +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..eb6a9ad --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "flag" + "log" + "net/http" + "os" + + "forge.cadoles.com/Cadoles/ldap-profile/cmd/server/config" + "forge.cadoles.com/wpetit/goweb/middleware/container" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/pkg/errors" +) + +var ( + configFile = "" + workdir = "" + dumpConfig = false +) + +func init() { + flag.StringVar(&configFile, "config", configFile, "configuration file") + flag.StringVar(&workdir, "workdir", workdir, "working directory") + flag.BoolVar(&dumpConfig, "dump-config", dumpConfig, "dump configuration and exit") +} + +func main() { + + flag.Parse() + + // Switch to new working directory if defined + if workdir != "" { + if err := os.Chdir(workdir); err != nil { + panic(errors.Wrapf(err, "error while changing working directory to '%s'", workdir)) + } + } + + // Load configuration file if defined, use default configuration otherwise + var conf *config.Config + var err error + if configFile != "" { + conf, err = config.NewFromFile(configFile) + if err != nil { + panic(errors.Wrapf(err, "error while loading config file '%s'", configFile)) + } + } else { + conf = config.NewDefault() + } + + // Dump configuration if asked + if dumpConfig { + if err := config.Dump(conf, os.Stdout); err != nil { + panic(errors.Wrap(err, "error while dumping config")) + } + os.Exit(0) + } + + r := chi.NewRouter() + + // Define base middlewares + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + // Create service container + ctn, err := getServiceContainer(conf) + if err != nil { + panic(errors.Wrap(err, "error while creating service container")) + } + + // Expose service container on router + r.Use(container.ServiceContainer(ctn)) + + // Define routes + mountRoutes(r, conf) + + log.Printf("listening on '%s'", conf.HTTP.Address) + if err := http.ListenAndServe(conf.HTTP.Address, r); err != nil { + panic(errors.Wrapf(err, "error while listening on '%s'", conf.HTTP.Address)) + } + +} diff --git a/cmd/server/public/css/common.css b/cmd/server/public/css/common.css new file mode 100644 index 0000000..348648a --- /dev/null +++ b/cmd/server/public/css/common.css @@ -0,0 +1,3 @@ +.has-margin-top-small { + margin-top: 1rem; +} \ No newline at end of file diff --git a/cmd/server/public/css/login.css b/cmd/server/public/css/login.css new file mode 100644 index 0000000..7851b6d --- /dev/null +++ b/cmd/server/public/css/login.css @@ -0,0 +1,17 @@ +body { + background-color: #fafafa; +} +.login .box { + margin-top: 5rem; +} +.login .avatar { + margin-top: -70px; + padding-bottom: 20px; +} +.login .avatar img { + padding: 5px; + background: #fff; + border-radius: 50%; + -webkit-box-shadow: 0 2px 3px rgba(10,10,10,.1), 0 0 0 1px rgba(10,10,10,.1); + box-shadow: 0 2px 3px rgba(10,10,10,.1), 0 0 0 1px rgba(10,10,10,.1); +} \ No newline at end of file diff --git a/cmd/server/public/css/profile.css b/cmd/server/public/css/profile.css new file mode 100644 index 0000000..e69de29 diff --git a/cmd/server/public/img/logo.svg b/cmd/server/public/img/logo.svg new file mode 100644 index 0000000..be6cbdb --- /dev/null +++ b/cmd/server/public/img/logo.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cmd/server/route.go b/cmd/server/route.go new file mode 100644 index 0000000..8a76b65 --- /dev/null +++ b/cmd/server/route.go @@ -0,0 +1,212 @@ +package main + +import ( + "log" + "net/http" + + "forge.cadoles.com/Cadoles/ldap-profile/cmd/server/config" + "forge.cadoles.com/Cadoles/ldap-profile/ldap" + "forge.cadoles.com/wpetit/goweb/middleware/container" + "forge.cadoles.com/wpetit/goweb/service/session" + "forge.cadoles.com/wpetit/goweb/service/template" + "forge.cadoles.com/wpetit/goweb/static" + "forge.cadoles.com/wpetit/goweb/template/html" + "github.com/go-chi/chi" + "github.com/pkg/errors" + ldapv3 "gopkg.in/ldap.v3" +) + +func mountRoutes(r *chi.Mux, config *config.Config) { + r.Get("/login", serveLoginPage) + r.Post("/login", handleLoginForm) + r.Get("/logout", handleLogout) + r.Group(func(r chi.Router) { + r.Use(authMiddleware) + r.Get("/", serveHomePage) + r.Get("/profile", serveProfilePage) + }) + r.Get("/*", static.Dir(config.HTTP.PublicDir, "", r.NotFoundHandler())) +} + +func serveHomePage(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/profile", http.StatusTemporaryRedirect) +} + +func serveLoginPage(w http.ResponseWriter, r *http.Request) { + ctn := container.Must(r.Context()) + tmpl := template.Must(ctn) + if err := tmpl.RenderPage(w, "login.html.tmpl", nil); err != nil { + panic(errors.Wrap(err, "error while rendering page")) + } +} + +func handleLogout(w http.ResponseWriter, r *http.Request) { + ctn := container.Must(r.Context()) + sess, err := session.Must(ctn).Get(w, r) + if err != nil { + panic(errors.Wrap(err, "error while retrieving session")) + } + if err := sess.Delete(w, r); err != nil { + panic(errors.Wrap(err, "error while deleting session")) + } + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) +} + +func handleLoginForm(w http.ResponseWriter, r *http.Request) { + + if err := r.ParseForm(); err != nil { + panic(errors.Wrap(err, "error while parsing form")) + } + + username := r.Form.Get("username") + password := r.Form.Get("password") + + ctn := container.Must(r.Context()) + ldapSrv := ldap.Must(ctn) + tmplSrv := template.Must(ctn) + conf := config.Must(ctn) + + sess, err := session.Must(ctn).Get(w, r) + if err != nil { + panic(errors.Wrap(err, "error while retrieving session")) + } + + renderInvalidCredentials := func() { + sess.AddFlash(session.FlashError, "Identifiants invalides.") + data := extendTemplateData(w, r, template.Data{ + "Username": username, + }) + if err := tmplSrv.RenderPage(w, "login.html.tmpl", data); err != nil { + panic(errors.Wrap(err, "error while rendering page")) + } + } + + results, err := ldapSrv.Search( + ldap.EscapeFilter(conf.LDAP.UserSearchFilterPattern, username), + ldap.WithBaseDN(conf.LDAP.BaseDN), + ldap.WithScope(ldapv3.ScopeWholeSubtree), + ldap.WithSizeLimit(1), + ldap.WithAttributes("dn"), + ) + if err != nil { + panic(errors.Wrap(err, "error while searching ldap entry")) + } + + if len(results.Entries) == 0 { + renderInvalidCredentials() + return + } + + userDN := results.Entries[0].DN + + log.Printf("authenticating user '%s' with DN '%s'", username, userDN) + + if err := ldapSrv.Bind(userDN, password); err != nil { + + // If the provided credentials are invalid, add flash message and rerender + // the page + if ldapv3.IsErrorWithCode(errors.Cause(err), ldapv3.LDAPResultInvalidCredentials) { + renderInvalidCredentials() + return + } + + if ldapv3.IsErrorWithCode(errors.Cause(err), ldapv3.LDAPResultNoSuchObject) { + renderInvalidCredentials() + return + } + + panic(errors.Wrap(err, "error while binding ldap connection")) + } + + log.Printf("successful authentication for user with DN '%s'", userDN) + + sess.Set("password", password) + sess.Set("dn", userDN) + sess.AddFlash(session.FlashSuccess, "Bienvenue !") + + if err := sess.Save(w, r); err != nil { + panic(errors.Wrap(err, "error while saving session")) + } + + http.Redirect(w, r, "/", http.StatusSeeOther) + +} + +func serveProfilePage(w http.ResponseWriter, r *http.Request) { + ctn := container.Must(r.Context()) + + sess, err := session.Must(ctn).Get(w, r) + if err != nil { + panic(errors.Wrap(err, "error while retrieving session")) + } + + ldapSrv := ldap.Must(ctn) + + conn, err := ldapSrv.Connect() + if err != nil { + panic(errors.Wrap(err, "error while connecting to ldap server")) + } + defer conn.Close() + + userDN := sess.Get("dn").(string) + password := sess.Get("password").(string) + + if err := ldapSrv.BindConn(conn, userDN, password); err != nil { + panic(errors.Wrap(err, "error while binding ldap connection")) + } + + conf := config.Must(ctn) + + results, err := ldapSrv.Search( + "(&)", + ldap.WithBaseDN(userDN), + ldap.WithScope(ldapv3.ScopeBaseObject), + ldap.WithSizeLimit(1), + ldap.WithAttributes(conf.LDAP.EditableAttributes...), + ) + if err != nil { + panic(errors.Wrap(err, "error while searching ldap entry")) + } + + if len(results.Entries) == 0 { + panic(errors.Errorf("could not retrieve ldap entry '%s'", userDN)) + } + + tmpl := template.Must(ctn) + + data := extendTemplateData(w, r, template.Data{ + "EntryAttributes": results.Entries[0].Attributes, + }) + + if err := tmpl.RenderPage(w, "profile.html.tmpl", data); err != nil { + panic(errors.Wrap(err, "error while rendering page")) + } +} + +func authMiddleware(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + ctn := container.Must(r.Context()) + sess, err := session.Must(ctn).Get(w, r) + if err != nil { + panic(errors.Wrap(err, "error while retrieving session")) + } + dn, ok := sess.Get("dn").(string) + if !ok || dn == "" { + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) + return + } + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) +} + +func extendTemplateData(w http.ResponseWriter, r *http.Request, data template.Data) template.Data { + ctn := container.Must(r.Context()) + data, err := template.Extend(data, + html.WithFlashes(w, r, ctn), + ) + if err != nil { + panic(errors.Wrap(err, "error while extending template data")) + } + return data +} diff --git a/cmd/server/templates/blocks/base.html.tmpl b/cmd/server/templates/blocks/base.html.tmpl new file mode 100644 index 0000000..ad6a633 --- /dev/null +++ b/cmd/server/templates/blocks/base.html.tmpl @@ -0,0 +1,19 @@ +{{define "base"}} + + + + + + {{block "title" . -}}{{- end}} + + + {{- block "head_style" . -}}{{end}} + + {{- block "head_script" . -}}{{end}} + + + {{- block "body" . -}}{{- end -}} + {{- block "body_script" . -}}{{end}} + + +{{end}} \ No newline at end of file diff --git a/cmd/server/templates/blocks/flash.html.tmpl b/cmd/server/templates/blocks/flash.html.tmpl new file mode 100644 index 0000000..2369849 --- /dev/null +++ b/cmd/server/templates/blocks/flash.html.tmpl @@ -0,0 +1,23 @@ +{{define "flash"}} +
+ {{- range .Flashes -}} + {{- if eq .Type "error" -}} + {{template "flash_message" map "Title" "Erreur" "MessageClass" "is-danger" "Message" .Message }} + {{- else if eq .Type "warn" -}} + {{template "flash_message" map "Title" "Attention" "MessageClass" "is-warning" "Message" .Message }} + {{- else if eq .Type "success" -}} + {{template "flash_message" map "Title" "Succès" "MessageClass" "is-success" "Message" .Message }} + {{- else -}} + {{template "flash_message" map "Title" "Information" "MessageClass" "is-info" "Message" .Message }} + {{- end -}} + {{- end -}} +
+{{end}} + +{{define "flash_message" -}} +
+
+ {{.Title}} {{.Message}} +
+
+{{- end}} \ No newline at end of file diff --git a/cmd/server/templates/layouts/login.html.tmpl b/cmd/server/templates/layouts/login.html.tmpl new file mode 100644 index 0000000..c8d2f03 --- /dev/null +++ b/cmd/server/templates/layouts/login.html.tmpl @@ -0,0 +1,41 @@ +{{define "title"}}LDAP Profile - Authentification{{end}} +{{define "head_style"}} + +{{end}} +{{define "body"}} +
+
+
+
+ {{template "flash" .}} +
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+{{end}} +{{template "base" .}} \ No newline at end of file diff --git a/cmd/server/templates/layouts/profile.html.tmpl b/cmd/server/templates/layouts/profile.html.tmpl new file mode 100644 index 0000000..1adb830 --- /dev/null +++ b/cmd/server/templates/layouts/profile.html.tmpl @@ -0,0 +1,46 @@ +{{define "title"}}LDAP Profile - Votre profil{{end}} +{{define "head_style"}} + +{{end}} +{{define "body"}} +
+
+
+ {{template "flash" .}} + +
+
+

Votre profil

+
+ {{range .EntryAttributes}} + + {{ $name := .Name }} + {{range .Values}} +
+
+ +
+ +
+ {{end}} +
+ +
+ {{end}} +
+ +
+
+
+
+
+
+
+{{end}} +{{template "base" .}} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0a8bea8 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module forge.cadoles.com/Cadoles/ldap-profile + +go 1.12 + +require ( + forge.cadoles.com/wpetit/goweb v0.0.0-20190513070928-35c763a6c65f + github.com/go-chi/chi v4.0.2+incompatible + github.com/gorilla/sessions v1.1.3 + github.com/oxtoacart/bpool v0.0.0-20190227141107-8c4636f812cc // indirect + github.com/pkg/errors v0.8.1 + github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect + golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 // indirect + gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect + gopkg.in/ini.v1 v1.42.0 + gopkg.in/ldap.v3 v3.0.3 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4de1c84 --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +forge.cadoles.com/wpetit/goweb v0.0.0-20181228104530-7c00e0c8bf14 h1:Tb3q3SAEuIg4EVnRCPA/BuLc2Ppe85bEjIBQRKIR7Bg= +forge.cadoles.com/wpetit/goweb v0.0.0-20181228104530-7c00e0c8bf14/go.mod h1:0zrl4O5z1OWAlQYtFF8/O/iGpCMsiDmbXx3ZO+PNG3o= +forge.cadoles.com/wpetit/goweb v0.0.0-20190513070928-35c763a6c65f h1:TUEsmlYvv5RkPEsK63oiCqwoju72M8tpd+xjjQ1wLag= +forge.cadoles.com/wpetit/goweb v0.0.0-20190513070928-35c763a6c65f/go.mod h1:0zrl4O5z1OWAlQYtFF8/O/iGpCMsiDmbXx3ZO+PNG3o= +github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= +github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= +github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/oxtoacart/bpool v0.0.0-20190227141107-8c4636f812cc h1:uhnyuvDwdKbjemAXHKsiEZOPagHim2nRjMcazH1g26A= +github.com/oxtoacart/bpool v0.0.0-20190227141107-8c4636f812cc/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak= +golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= +gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk= +gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ldap.v3 v3.0.3 h1:YKRHW/2sIl05JsCtx/5ZuUueFuJyoj/6+DGXe3wp6ro= +gopkg.in/ldap.v3 v3.0.3/go.mod h1:oxD7NyBuxchC+SgJDE1Q5Od05eGt29SDQVBmV+HYbzw= diff --git a/ldap/provider.go b/ldap/provider.go new file mode 100644 index 0000000..a2bcd6d --- /dev/null +++ b/ldap/provider.go @@ -0,0 +1,12 @@ +package ldap + +import "forge.cadoles.com/wpetit/goweb/service" + +func ServiceProvider(url string) service.Provider { + srv := &Service{ + url: url, + } + return func(ctn *service.Container) (interface{}, error) { + return srv, nil + } +} diff --git a/ldap/search.go b/ldap/search.go new file mode 100644 index 0000000..6e9016e --- /dev/null +++ b/ldap/search.go @@ -0,0 +1,65 @@ +package ldap + +import ( + "fmt" + + ldap "gopkg.in/ldap.v3" +) + +type SearchOptions struct { + BaseDN string + Scope int + DerefAliases int + SizeLimit int + TimeLimit int + TypesOnly bool + Attributes []string + Controls []ldap.Control +} + +type SearchOptionFunc func(opts *SearchOptions) + +func WithBaseDN(dn string) SearchOptionFunc { + return func(opts *SearchOptions) { + opts.BaseDN = dn + } +} + +func WithSizeLimit(sizeLimit int) SearchOptionFunc { + return func(opts *SearchOptions) { + opts.SizeLimit = sizeLimit + } +} + +func WithAttributes(attributes ...string) SearchOptionFunc { + return func(opts *SearchOptions) { + opts.Attributes = attributes + } +} + +func WithScope(scope int) SearchOptionFunc { + return func(opts *SearchOptions) { + opts.Scope = scope + } +} + +func EscapeFilter(pattern string, values ...string) string { + escapedValues := make([]interface{}, len(values)) + for i, v := range values { + escapedValues[i] = ldap.EscapeFilter(v) + } + return fmt.Sprintf(pattern, escapedValues...) +} + +func defaultSearchOptions() *SearchOptions { + return &SearchOptions{ + BaseDN: "", + Scope: ldap.ScopeSingleLevel, + DerefAliases: ldap.NeverDerefAliases, + SizeLimit: 0, + TimeLimit: 0, + TypesOnly: false, + Attributes: make([]string, 0), + Controls: make([]ldap.Control, 0), + } +} diff --git a/ldap/service.go b/ldap/service.go new file mode 100644 index 0000000..2338369 --- /dev/null +++ b/ldap/service.go @@ -0,0 +1,91 @@ +package ldap + +import ( + "forge.cadoles.com/wpetit/goweb/service" + "github.com/pkg/errors" + ldap "gopkg.in/ldap.v3" +) + +const ServiceName service.Name = "ldap" + +type Service struct { + url string +} + +func (s *Service) Search(filter string, opts ...SearchOptionFunc) (*ldap.SearchResult, error) { + conn, err := s.Connect() + if err != nil { + return nil, errors.Wrap(err, "error while connecting to ldap server") + } + defer conn.Close() + return s.SearchConn(conn, filter, opts...) +} + +func (s *Service) SearchConn(conn *ldap.Conn, filter string, opts ...SearchOptionFunc) (*ldap.SearchResult, error) { + options := defaultSearchOptions() + for _, optFunc := range opts { + optFunc(options) + } + req := ldap.NewSearchRequest( + options.BaseDN, + options.Scope, + options.DerefAliases, + options.SizeLimit, + options.TimeLimit, + options.TypesOnly, + filter, + options.Attributes, + options.Controls, + ) + result, err := conn.Search(req) + if err != nil { + return nil, errors.Wrap(err, "error while executing ldap search") + } + return result, nil +} + +func (s *Service) Bind(dn, password string) error { + conn, err := s.Connect() + if err != nil { + return errors.Wrap(err, "error while connecting to ldap server") + } + defer conn.Close() + return s.BindConn(conn, dn, password) +} + +func (s *Service) BindConn(conn *ldap.Conn, dn, password string) error { + if err := conn.Bind(dn, password); err != nil { + return errors.Wrap(err, "error while executing ldap bind") + } + return nil +} + +func (s *Service) Connect() (*ldap.Conn, error) { + conn, err := ldap.DialURL(s.url) + if err != nil { + return nil, errors.Wrapf(err, "error while dialing ldap url '%s'", s.url) + } + return conn, err +} + +// From retrieves the ldap service in the given container +func From(container *service.Container) (*Service, error) { + service, err := container.Service(ServiceName) + if err != nil { + return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName) + } + srv, ok := service.(*Service) + if !ok { + return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName) + } + return srv, nil +} + +// Must retrieves the ldap service in the given container or panic otherwise +func Must(container *service.Container) *Service { + srv, err := From(container) + if err != nil { + panic(err) + } + return srv +} diff --git a/modd.conf b/modd.conf new file mode 100644 index 0000000..ba23ea7 --- /dev/null +++ b/modd.conf @@ -0,0 +1,9 @@ +**/*.go +cmd/server/templates/**/* +data/server.conf +modd.conf +Makefile { + prep: make bin/server + prep: [ -e data/server.conf ] || ( mkdir -p data && bin/server -dump-config > data/server.conf ) + daemon: bin/server -workdir "./cmd/server" -config ../../data/server.conf +}