Initial commit

This commit is contained in:
wpetit 2019-05-13 09:19:33 +02:00
commit 91feda6471
22 changed files with 946 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/bin
/data
/vendor

40
Makefile Normal file
View File

@ -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

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

44
cmd/server/container.go Normal file
View File

@ -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
}

82
cmd/server/main.go Normal file
View File

@ -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))
}
}

View File

@ -0,0 +1,3 @@
.has-margin-top-small {
margin-top: 1rem;
}

View File

@ -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);
}

View File

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="42.337349"
height="40.452461"
id="svg3780"
version="1.1"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="logo.svg">
<defs
id="defs3782" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.2416268"
inkscape:cx="-57.508499"
inkscape:cy="19.200675"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="2452"
inkscape:window-height="1306"
inkscape:window-x="108"
inkscape:window-y="60"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid3015"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
originx="-8.39956"
originy="-37.937141"
spacingx="1"
spacingy="1" />
</sodipodi:namedview>
<metadata
id="metadata3785">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-335.39956,-499.83558)">
<path
style="display:inline;fill:#078eb5;fill-opacity:1;stroke:none;enable-background:new"
d="m 356.56825,499.83558 c -13.23043,0 -21.16869,13.23042 -21.16869,23.81477 h 2.64609 v 10.58433 c 5.29217,7.93824 32.07212,8.20297 37.04519,0 v -10.58433 h 2.64607 c 0,-4.29058 -1.30219,-9.01755 -3.73483,-13.12018 l -2.99063,3.03198 v 7.12513 l 1.61247,2.21885 -2.39802,2.31533 -2.43937,-2.31533 1.72272,-2.14993 -0.0688,-7.71776 3.87264,-3.6108 c -0.97461,-1.47147 -2.10693,-2.84403 -3.37655,-4.0656 l -4.87871,5.18192 v 7.13891 l 1.61245,2.21886 -2.41179,2.31532 -2.43936,-2.31532 1.73648,-2.16374 -0.0826,-7.71774 5.37486,-5.63672 c -1.29755,-1.09094 -2.7222,-2.03518 -4.27232,-2.77012 l -5.59538,5.93992 v 7.12513 l 1.62625,2.23264 -2.4118,2.31532 -2.43936,-2.31532 1.73649,-2.16373 -0.0826,-7.71774 5.88478,-5.95371 c -2.05509,-0.79293 -4.29761,-1.24034 -6.72546,-1.24034 z m 6.72545,1.24034 1.2817,0.53749 z m 5.55403,3.30761 1.08875,0.97851 z m 4.46526,5.04411 0.68909,1.10253 z M 352,526.57486 l 8.85806,-0.11137 0.0557,10.43027 -8.91375,0.1018 z"
id="path3773"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scccccccccccccccccccccccccccccccccscccccccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

212
cmd/server/route.go Normal file
View File

@ -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
}

View File

@ -0,0 +1,19 @@
{{define "base"}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{block "title" . -}}{{- end}}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
<link rel="stylesheet" href="/css/common.css">
{{- block "head_style" . -}}{{end}}
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
{{- block "head_script" . -}}{{end}}
</head>
<body>
{{- block "body" . -}}{{- end -}}
{{- block "body_script" . -}}{{end}}
</body>
</html>
{{end}}

View File

@ -0,0 +1,23 @@
{{define "flash"}}
<div class="flash has-margin-top-small">
{{- 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 -}}
</div>
{{end}}
{{define "flash_message" -}}
<div class="message {{.MessageClass}}">
<div class="message-body">
<span class="has-text-weight-bold">{{.Title}}</span> {{.Message}}
</div>
</div>
{{- end}}

View File

@ -0,0 +1,41 @@
{{define "title"}}LDAP Profile - Authentification{{end}}
{{define "head_style"}}
<link rel="stylesheet" href="/css/login.css" />
{{end}}
{{define "body"}}
<section class="hero is-fullheight login">
<div class="hero-body">
<div class="container">
<div class="column is-4 is-offset-4">
{{template "flash" .}}
<div class="has-text-centered has-margin-top-small">
<div class="box">
<figure class="avatar">
<img src="/img/logo.svg" width="128" height="128">
</figure>
<form method="POST">
<div class="field">
<div class="control">
<input class="input is-normal"
name="username" type="text"
value="{{ .Username }}"
placeholder="Votre identifiant" autofocus="">
</div>
</div>
<div class="field">
<div class="control">
<input class="input is-normal"
name="password" type="password"
placeholder="Votre mot de passe">
</div>
</div>
<button class="button is-block is-info is-normal is-fullwidth">S'identifier</button>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
{{end}}
{{template "base" .}}

View File

@ -0,0 +1,46 @@
{{define "title"}}LDAP Profile - Votre profil{{end}}
{{define "head_style"}}
<link rel="stylesheet" href="/css/profile.css" />
{{end}}
{{define "body"}}
<section class="is-fullheight profile">
<div class="container">
<div class="column is-8 is-offset-2">
{{template "flash" .}}
<div class="has-text-right">
<a href="/logout" class="button is-warning">Se déconnecter</a>
</div>
<div class="has-margin-top-small">
<div class="box">
<h2 class="title is-2">Votre profil</h2>
<form method="POST">
{{range .EntryAttributes}}
<label class="label">{{ .Name }}</label>
{{ $name := .Name }}
{{range .Values}}
<div class="field has-addons">
<div class="control is-expanded">
<input class="input" type="text" value="{{ . }}" name="{{ $name }}">
</div>
<div class="control">
<a class="button is-danger">
-
</a>
</div>
</div>
{{end}}
<div class="has-text-right">
<button data-attribute-name="{{ $name }}" class="button is-primary">+</button>
</div>
{{end}}
<div class="has-text-right has-margin-top-small">
<button class="button is-medium is-success">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
{{end}}
{{template "base" .}}

16
go.mod Normal file
View File

@ -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
)

38
go.sum Normal file
View File

@ -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=

12
ldap/provider.go Normal file
View File

@ -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
}
}

65
ldap/search.go Normal file
View File

@ -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),
}
}

91
ldap/service.go Normal file
View File

@ -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
}

9
modd.conf Normal file
View File

@ -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
}