aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2020-02-09 22:06:33 +0100
committerAlex Auvolat <alex@adnab.me>2020-02-09 22:06:33 +0100
commit43825b1bbc02e9b1697b965a1621a936c5ae0334 (patch)
tree19945695086a20c8f8aedda7c90e171b61638b41
parentf929ca7297905e4233aa32fe2d80dd4cc3fcda30 (diff)
downloadguichet-43825b1bbc02e9b1697b965a1621a936c5ae0334.tar.gz
guichet-43825b1bbc02e9b1697b965a1621a936c5ae0334.zip
LDAP modification form
-rw-r--r--admin.go287
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--main.go24
-rw-r--r--templates/admin_groups.html31
-rw-r--r--templates/admin_ldap.html134
-rw-r--r--templates/admin_users.html8
-rw-r--r--templates/home.html2
8 files changed, 466 insertions, 23 deletions
diff --git a/admin.go b/admin.go
index 5a86fe2..e6a55f5 100644
--- a/admin.go
+++ b/admin.go
@@ -1,12 +1,14 @@
package main
import (
+ "strings"
"fmt"
"html/template"
"net/http"
"sort"
"github.com/go-ldap/ldap/v3"
+ "github.com/gorilla/mux"
)
func checkAdminLogin(w http.ResponseWriter, r *http.Request) *LoginStatus {
@@ -30,16 +32,31 @@ func checkAdminLogin(w http.ResponseWriter, r *http.Request) *LoginStatus {
return login
}
+type EntryList []*ldap.Entry
+
+func (d EntryList) Len() int {
+ return len(d)
+}
+
+func (d EntryList) Swap(i, j int) {
+ d[i], d[j] = d[j], d[i]
+}
+
+func (d EntryList) Less(i, j int) bool {
+ return d[i].DN < d[j].DN
+}
+
+
type AdminUsersTplData struct {
Login *LoginStatus
UserNameAttr string
- Users []*ldap.Entry
+ Users EntryList
}
func handleAdminUsers(w http.ResponseWriter, r *http.Request) {
templateAdminUsers := template.Must(template.ParseFiles("templates/layout.html", "templates/admin_users.html"))
- login := checkLogin(w, r)
+ login := checkAdminLogin(w, r)
if login == nil {
return
}
@@ -60,22 +77,270 @@ func handleAdminUsers(w http.ResponseWriter, r *http.Request) {
data := &AdminUsersTplData{
Login: login,
UserNameAttr: config.UserNameAttr,
- Users: sr.Entries,
+ Users: EntryList(sr.Entries),
}
- sort.Sort(data)
+ sort.Sort(data.Users)
templateAdminUsers.Execute(w, data)
}
-func (d *AdminUsersTplData) Len() int {
- return len(d.Users)
+type AdminGroupsTplData struct {
+ Login *LoginStatus
+ GroupNameAttr string
+ Groups EntryList
}
-func (d *AdminUsersTplData) Swap(i, j int) {
- d.Users[i], d.Users[j] = d.Users[j], d.Users[i]
+func handleAdminGroups(w http.ResponseWriter, r *http.Request) {
+ templateAdminGroups := template.Must(template.ParseFiles("templates/layout.html", "templates/admin_groups.html"))
+
+ login := checkAdminLogin(w, r)
+ if login == nil {
+ return
+ }
+
+ searchRequest := ldap.NewSearchRequest(
+ config.GroupBaseDN,
+ ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false,
+ fmt.Sprintf("(&(objectClass=groupOfNames))"),
+ []string{config.GroupNameAttr, "dn", "displayname"},
+ nil)
+
+ sr, err := login.conn.Search(searchRequest)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ data := &AdminGroupsTplData{
+ Login: login,
+ GroupNameAttr: config.GroupNameAttr,
+ Groups: EntryList(sr.Entries),
+ }
+ sort.Sort(data.Groups)
+
+ templateAdminGroups.Execute(w, data)
}
-func (d *AdminUsersTplData) Less(i, j int) bool {
- return d.Users[i].GetAttributeValue(config.UserNameAttr) <
- d.Users[j].GetAttributeValue(config.UserNameAttr)
+type AdminLDAPTplData struct {
+ DN string
+ Members []string
+ Groups []string
+ Props map[string]*PropValues
+ Children []Child
+ Path []PathItem
+ AddError string
+}
+
+type Child struct {
+ DN string
+ Identifier string
+ DisplayName string
+}
+
+type PathItem struct {
+ DN string
+ Identifier string
+ Active bool
+}
+
+type PropValues struct {
+ Values []string
+ Editable bool
+ ModifySuccess bool
+ ModifyError string
+}
+
+func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
+ templateAdminLDAP := template.Must(template.ParseFiles("templates/layout.html", "templates/admin_ldap.html"))
+
+ login := checkAdminLogin(w, r)
+ if login == nil {
+ return
+ }
+
+ dn := mux.Vars(r)["dn"]
+
+ modifyAttr := ""
+ modifyError := ""
+ modifySuccess := false
+ addError := ""
+
+ if r.Method == "POST" {
+ r.ParseForm()
+ action := strings.Join(r.Form["action"], "")
+ if action == "modify" {
+ attr := strings.Join(r.Form["attr"], "")
+ values := strings.Split(strings.Join(r.Form["values"], ""), "\n")
+ values_filtered := []string{}
+ for _, v := range values {
+ v2 := strings.TrimSpace(v)
+ if v2 != "" {
+ values_filtered = append(values_filtered, v2)
+ }
+ }
+
+ modifyAttr = attr
+ if len(values_filtered) == 0 {
+ modifyError = "Refusing to delete attribute."
+ } else {
+ modify_request := ldap.NewModifyRequest(dn, nil)
+ modify_request.Replace(attr, values_filtered)
+
+ err := login.conn.Modify(modify_request)
+ if err != nil {
+ modifyError = err.Error()
+ } else {
+ modifySuccess = true
+ }
+ }
+ } else if action == "add" {
+ attr := strings.Join(r.Form["attr"], "")
+ values := strings.Split(strings.Join(r.Form["values"], ""), "\n")
+ values_filtered := []string{}
+ for _, v := range values {
+ v2 := strings.TrimSpace(v)
+ if v2 != "" {
+ values_filtered = append(values_filtered, v2)
+ }
+ }
+
+ modify_request := ldap.NewModifyRequest(dn, nil)
+ modify_request.Add(attr, values_filtered)
+
+ err := login.conn.Modify(modify_request)
+ modifyAttr = attr
+ if err != nil {
+ addError = err.Error()
+ }
+ } else if action == "delete" {
+ attr := strings.Join(r.Form["attr"], "")
+
+ modify_request := ldap.NewModifyRequest(dn, nil)
+ modify_request.Replace(attr, []string{})
+
+ err := login.conn.Modify(modify_request)
+ if err != nil {
+ modifyError = err.Error()
+ }
+ }
+ }
+
+ // Build path
+ path := []PathItem{
+ PathItem{
+ DN: config.BaseDN,
+ Identifier: config.BaseDN,
+ Active: dn == config.BaseDN,
+ },
+ }
+
+ len_base_dn := len(strings.Split(config.BaseDN, ","))
+ dn_split := strings.Split(dn, ",")
+ dn_last_attr := strings.Split(dn_split[0], "=")[0]
+ for i := len_base_dn + 1; i <= len(dn_split); i++ {
+ path = append(path, PathItem{
+ DN: strings.Join(dn_split[len(dn_split)-i:len(dn_split)], ","),
+ Identifier: dn_split[len(dn_split)-i],
+ Active: i == len(dn_split),
+ })
+ }
+
+ // Get object and parse it
+ searchRequest := ldap.NewSearchRequest(
+ dn,
+ ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
+ fmt.Sprintf("(objectclass=*)"),
+ []string{},
+ nil)
+
+ sr, err := login.conn.Search(searchRequest)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ if len(sr.Entries) != 1 {
+ http.Error(w, fmt.Sprintf("%d objects found", len(sr.Entries)), http.StatusInternalServerError)
+ return
+ }
+
+ object := sr.Entries[0]
+
+ props := make(map[string]*PropValues)
+ for _, attr := range object.Attributes {
+ if attr.Name != dn_last_attr {
+ if existing, ok := props[attr.Name]; ok {
+ existing.Values = append(existing.Values, attr.Values...)
+ } else {
+ editable := true
+ for _, restricted := range []string{
+ "creatorsname", "modifiersname", "createtimestamp",
+ "modifytimestamp", "entryuuid",
+ } {
+ if strings.EqualFold(attr.Name, restricted) {
+ editable = false
+ break
+ }
+ }
+ pv := &PropValues{
+ Values: attr.Values,
+ Editable: editable,
+ }
+ if attr.Name == modifyAttr {
+ if modifySuccess {
+ pv.ModifySuccess = true
+ } else if modifyError != "" {
+ pv.ModifyError = modifyError
+ }
+ }
+ props[attr.Name] = pv
+ }
+ }
+ }
+
+ members := []string{}
+ if mp, ok := props["member"]; ok {
+ members = mp.Values
+ delete(props, "member")
+ }
+ groups := []string{}
+ if gp, ok := props["memberof"]; ok {
+ groups = gp.Values
+ delete(props, "memberof")
+ }
+
+ // Get children
+ searchRequest = ldap.NewSearchRequest(
+ dn,
+ ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false,
+ fmt.Sprintf("(objectclass=*)"),
+ []string{"dn", "displayname"},
+ nil)
+
+ sr, err = login.conn.Search(searchRequest)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ sort.Sort(EntryList(sr.Entries))
+
+ children := []Child{}
+ for _, item := range sr.Entries {
+ children = append(children, Child{
+ DN: item.DN,
+ Identifier: strings.Split(item.DN, ",")[0],
+ DisplayName: item.GetAttributeValue("displayname"),
+ })
+ }
+
+ templateAdminLDAP.Execute(w, &AdminLDAPTplData{
+ DN: dn,
+ Members: members,
+ Groups: groups,
+ Props: props,
+ Children: children,
+ Path: path,
+ AddError: addError,
+ })
}
diff --git a/go.mod b/go.mod
index ab70c69..b11c004 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.13
require (
github.com/go-ldap/ldap v3.0.3+incompatible
github.com/go-ldap/ldap/v3 v3.1.6
+ github.com/gorilla/mux v1.7.3
github.com/gorilla/sessions v1.2.0
github.com/sirupsen/logrus v1.4.2
)
diff --git a/go.sum b/go.sum
index a0b62e4..8467403 100644
--- a/go.sum
+++ b/go.sum
@@ -5,6 +5,8 @@ github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHj
github.com/go-ldap/ldap v3.0.3+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-ldap/ldap/v3 v3.1.6 h1:VTihvB7egSAvU6KOagaiA/EvgJMR2jsjRAVIho2ydBo=
github.com/go-ldap/ldap/v3 v3.1.6/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
+github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
+github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
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.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
diff --git a/main.go b/main.go
index 29da525..a0d5b07 100644
--- a/main.go
+++ b/main.go
@@ -16,6 +16,7 @@ import (
"github.com/go-ldap/ldap/v3"
"github.com/gorilla/sessions"
+ "github.com/gorilla/mux"
)
type ConfigFile struct {
@@ -24,6 +25,7 @@ type ConfigFile struct {
LdapServerAddr string `json:"ldap_server_addr"`
LdapTLS bool `json:"ldap_tls"`
+ BaseDN string `json:"base_dn"`
UserBaseDN string `json:"user_base_dn"`
UserNameAttr string `json:"user_name_attr"`
GroupBaseDN string `json:"group_base_dn"`
@@ -53,6 +55,7 @@ func readConfig() ConfigFile {
SessionKey: base64.StdEncoding.EncodeToString(key_bytes),
LdapServerAddr: "ldap://127.0.0.1:389",
LdapTLS: false,
+ BaseDN: "dc=example,dc=com",
UserBaseDN: "ou=users,dc=example,dc=com",
UserNameAttr: "uid",
GroupBaseDN: "ou=groups,dc=example,dc=com",
@@ -103,19 +106,20 @@ func main() {
config = &config_file
store = sessions.NewFilesystemStore("", []byte(config.SessionKey))
- http.HandleFunc("/", handleHome)
- http.HandleFunc("/logout", handleLogout)
- http.HandleFunc("/profile", handleProfile)
- http.HandleFunc("/passwd", handlePasswd)
+ r := mux.NewRouter()
+ r.HandleFunc("/", handleHome)
+ r.HandleFunc("/logout", handleLogout)
+ r.HandleFunc("/profile", handleProfile)
+ r.HandleFunc("/passwd", handlePasswd)
- http.HandleFunc("/admin/users", handleAdminUsers)
- //http.HandleFunc("/admin/groups", handleAdminGroups)
- //http.HandleFunc("/admin/ldap", handleAdminLDAP)
+ r.HandleFunc("/admin/users", handleAdminUsers)
+ r.HandleFunc("/admin/groups", handleAdminGroups)
+ r.HandleFunc("/admin/ldap/{dn}", handleAdminLDAP)
staticfiles := http.FileServer(http.Dir("static"))
- http.Handle("/static/", http.StripPrefix("/static/", staticfiles))
+ r.Handle("/static/{file:.*}", http.StripPrefix("/static/", staticfiles))
- err := http.ListenAndServe(config.HttpBindAddr, logRequest(http.DefaultServeMux))
+ err := http.ListenAndServe(config.HttpBindAddr, logRequest(r))
if err != nil {
log.Fatal("Cannot start http server: ", err)
}
@@ -233,6 +237,7 @@ type HomePageData struct {
Login *LoginStatus
CanAdmin bool
CanInvite bool
+ BaseDN string
}
func handleHome(w http.ResponseWriter, r *http.Request) {
@@ -258,6 +263,7 @@ func handleHome(w http.ResponseWriter, r *http.Request) {
Login: login,
CanAdmin: can_admin,
CanInvite: can_invite,
+ BaseDN: config.BaseDN,
})
}
diff --git a/templates/admin_groups.html b/templates/admin_groups.html
new file mode 100644
index 0000000..8b5a4ef
--- /dev/null
+++ b/templates/admin_groups.html
@@ -0,0 +1,31 @@
+{{define "title"}}Liste des groupes |{{end}}
+
+{{define "body"}}
+
+<div class="d-flex">
+ <h4>Liste des groupes</h4>
+ <a class="ml-auto btn btn-info" href="/">Retour</a>
+</div>
+
+<table class="table mt-4">
+ <thead>
+ <th scope="col">Identifiant</th>
+ <th scope="col">Nom complet</th>
+ </thead>
+ <tbody>
+ {{with $root := .}}
+ {{range $group := $root.Groups}}
+ <tr>
+ <td>
+ <a href="/admin/ldap/{{$group.DN}}">
+ {{$group.GetAttributeValue $root.GroupNameAttr}}
+ </a>
+ </td>
+ <td>{{$group.GetAttributeValue "displayname"}}</td>
+ </tr>
+ {{end}}
+ {{end}}
+ </tbody>
+</table>
+
+{{end}}
diff --git a/templates/admin_ldap.html b/templates/admin_ldap.html
new file mode 100644
index 0000000..5eece8a
--- /dev/null
+++ b/templates/admin_ldap.html
@@ -0,0 +1,134 @@
+{{define "title"}}Explorateur LDAP |{{end}}
+
+{{define "body"}}
+
+<div class="d-flex">
+ <h4>Explorateur LDAP</h4>
+ <a class="ml-auto btn btn-info" href="/">Retour</a>
+</div>
+
+<div class="mt-4">
+ <nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ {{range .Path}}
+ {{if .Active}}
+ <li class="breadcrumb-item active" aria-current="page">{{.Identifier}}</li>
+ {{else}}
+ <li class="breadcrumb-item"><a href="/admin/ldap/{{.DN}}">{{.Identifier}}</a></li>
+ {{end}}
+ {{end}}
+ </ol>
+ </nav>
+</div>
+
+<table class="table mt-4">
+ <tbody>
+ {{range .Children}}
+ <tr>
+ <td>
+ <a href="/admin/ldap/{{.DN}}">
+ {{.Identifier}}
+ </a>
+ </td>
+ <td>{{.DisplayName}}</td>
+ </tr>
+ {{end}}
+ </tbody>
+</table>
+
+<h5>Attributs</h5>
+<div class="container">
+ {{range $key, $value := .Props}}
+ {{if $value.Editable}}
+ <div class="row mt-4">
+ <div class="col-md-3"><strong>{{$key}}</strong></div>
+
+ <div class="col-md-7">
+ <form method="POST">
+ <div class="form-row">
+ <input type="hidden" name="action" value="modify" />
+ <input type="hidden" name="attr" value="{{$key}}" />
+ <textarea name="values" rows="{{len $value.Values}}" class="form-control col-md-9">{{range $i, $x := $value.Values}}{{if $i}}{{"\n"}}{{end}}{{$x}}{{end}}</textarea>
+ <div class="col-md-3">
+ <input type="submit" value="Modifier" class="form-control btn btn-primary" />
+ </div>
+ </div>
+ </form>
+ {{if $value.ModifySuccess}}
+ <div class="alert alert-success mt-2">Modification enregistrée.</div>
+ {{end}}
+ {{if $value.ModifyError}}
+ <div class="alert alert-danger mt-2">
+ Impossible de modifier la valeur.
+ <div style="font-size: 0.8em">{{$value.ModifyError}}</div>
+ </div>
+ {{end}}
+ </div>
+
+ <div class="col-md-1">
+ <form method="POST" onsubmit="return confirm('Supprimer cet attribut ?');">
+ <input type="hidden" name="action" value="delete" />
+ <input type="hidden" name="attr" value="{{$key}}" />
+ <input type="submit" value="Suppr." class="form-control btn btn-danger btn-sm" />
+ </form>
+ </div>
+ </div>
+ {{end}}
+ {{end}}
+ {{range $key, $value := .Props}}
+ {{if not $value.Editable}}
+ <div class="row mt-4">
+ <div class="col-md-3"><strong>{{$key}}</strong></div>
+ <div class="col-md-9">
+ {{range $value.Values}}
+ <div>{{.}}</div>
+ {{end}}
+ </div>
+ </div>
+ {{end}}
+ {{end}}
+ <form method="POST">
+ <div class="row mt-4">
+ <div class="col-md-3">
+ <input type="hidden" name="action" value="add" />
+ <input class="form-control" type="text" name="attr" placeholder="Ajouter un attribut..." />
+ </div>
+ <div class="col-md-7">
+ {{if .AddError}}
+ <div class="alert alert-danger">
+ Impossible d'ajouter la valeur.
+ <div style="font-size: 0.8em">{{.AddError}}</div>
+ </div>
+ {{end}}
+ <div class="form-row">
+ <textarea name="values" placeholder="Valeur(s)..." rows="2" class="form-control col-md-9"></textarea>
+ <div class="col-md-3">
+ <input type="submit" value="Ajouter" class="form-control btn btn-success" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
+
+{{if .Members}}
+ <h5 class="mt-4">Membres</h5>
+ <ul class="list-group">
+ {{range .Members}}
+ <li class="list-group-item">{{.}}</li>
+ {{end}}
+ </ul>
+{{end}}
+
+{{if .Groups}}
+ <h5 class="mt-4">Membre de</h5>
+ <ul class="list-group">
+ {{range .Groups}}
+ <li class="list-group-item">{{.}}</li>
+ {{end}}
+ </ul>
+{{end}}
+
+<hr class="mt-4" />
+
+{{end}}
diff --git a/templates/admin_users.html b/templates/admin_users.html
index 39e291c..01d96d2 100644
--- a/templates/admin_users.html
+++ b/templates/admin_users.html
@@ -9,7 +9,7 @@
<table class="table mt-4">
<thead>
- <th scope="col">{{ .UserNameAttr }}</th>
+ <th scope="col">Identifiant</th>
<th scope="col">Nom complet</th>
<th scope="col">Email</th>
</thead>
@@ -17,7 +17,11 @@
{{with $root := .}}
{{range $user := $root.Users}}
<tr>
- <td>{{$user.GetAttributeValue $root.UserNameAttr}}</td>
+ <td>
+ <a href="/admin/ldap/{{$user.DN}}">
+ {{$user.GetAttributeValue $root.UserNameAttr}}
+ </a>
+ </td>
<td>{{$user.GetAttributeValue "displayname"}}</td>
<td>{{$user.GetAttributeValue "mail"}}</td>
</tr>
diff --git a/templates/home.html b/templates/home.html
index b4012fd..011dcc1 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -30,7 +30,7 @@
<div class="list-group list-group-flush">
<a class="list-group-item list-group-item-action" href="/admin/users">Utilisateurs</a>
<a class="list-group-item list-group-item-action" href="/admin/groups">Groupes</a>
- <a class="list-group-item list-group-item-action" href="/admin/ldap">Explorateur LDAP</a>
+ <a class="list-group-item list-group-item-action" href="/admin/ldap/{{.BaseDN}}">Explorateur LDAP</a>
</div>
</div>
{{end}}