Initial commit
This commit is contained in:
commit
91feda6471
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/bin
|
||||
/data
|
||||
/vendor
|
40
Makefile
Normal file
40
Makefile
Normal 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
|
68
cmd/server/config/config.go
Normal file
68
cmd/server/config/config.go
Normal 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
|
||||
}
|
9
cmd/server/config/provider.go
Normal file
9
cmd/server/config/provider.go
Normal 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
|
||||
}
|
||||
}
|
30
cmd/server/config/service.go
Normal file
30
cmd/server/config/service.go
Normal 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
44
cmd/server/container.go
Normal 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
82
cmd/server/main.go
Normal 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))
|
||||
}
|
||||
|
||||
}
|
3
cmd/server/public/css/common.css
Normal file
3
cmd/server/public/css/common.css
Normal file
@ -0,0 +1,3 @@
|
||||
.has-margin-top-small {
|
||||
margin-top: 1rem;
|
||||
}
|
17
cmd/server/public/css/login.css
Normal file
17
cmd/server/public/css/login.css
Normal 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);
|
||||
}
|
0
cmd/server/public/css/profile.css
Normal file
0
cmd/server/public/css/profile.css
Normal file
78
cmd/server/public/img/logo.svg
Normal file
78
cmd/server/public/img/logo.svg
Normal 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
212
cmd/server/route.go
Normal 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
|
||||
}
|
19
cmd/server/templates/blocks/base.html.tmpl
Normal file
19
cmd/server/templates/blocks/base.html.tmpl
Normal 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}}
|
23
cmd/server/templates/blocks/flash.html.tmpl
Normal file
23
cmd/server/templates/blocks/flash.html.tmpl
Normal 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}}
|
41
cmd/server/templates/layouts/login.html.tmpl
Normal file
41
cmd/server/templates/layouts/login.html.tmpl
Normal 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" .}}
|
46
cmd/server/templates/layouts/profile.html.tmpl
Normal file
46
cmd/server/templates/layouts/profile.html.tmpl
Normal 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
16
go.mod
Normal 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
38
go.sum
Normal 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
12
ldap/provider.go
Normal 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
65
ldap/search.go
Normal 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
91
ldap/service.go
Normal 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
9
modd.conf
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user