From 43825b1bbc02e9b1697b965a1621a936c5ae0334 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Sun, 9 Feb 2020 22:06:33 +0100 Subject: LDAP modification form --- admin.go | 287 ++++++++++++++++++++++++++++++++++++++++++-- go.mod | 1 + go.sum | 2 + main.go | 24 ++-- templates/admin_groups.html | 31 +++++ templates/admin_ldap.html | 134 +++++++++++++++++++++ templates/admin_users.html | 8 +- templates/home.html | 2 +- 8 files changed, 466 insertions(+), 23 deletions(-) create mode 100644 templates/admin_groups.html create mode 100644 templates/admin_ldap.html 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"}} + +
+

Liste des groupes

+ Retour +
+ + + + + + + + {{with $root := .}} + {{range $group := $root.Groups}} + + + + + {{end}} + {{end}} + +
IdentifiantNom complet
+ + {{$group.GetAttributeValue $root.GroupNameAttr}} + + {{$group.GetAttributeValue "displayname"}}
+ +{{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"}} + +
+

Explorateur LDAP

+ Retour +
+ +
+ +
+ + + + {{range .Children}} + + + + + {{end}} + +
+ + {{.Identifier}} + + {{.DisplayName}}
+ +
Attributs
+
+ {{range $key, $value := .Props}} + {{if $value.Editable}} +
+
{{$key}}
+ +
+
+
+ + + +
+ +
+
+
+ {{if $value.ModifySuccess}} +
Modification enregistrée.
+ {{end}} + {{if $value.ModifyError}} +
+ Impossible de modifier la valeur. +
{{$value.ModifyError}}
+
+ {{end}} +
+ +
+
+ + + +
+
+
+ {{end}} + {{end}} + {{range $key, $value := .Props}} + {{if not $value.Editable}} +
+
{{$key}}
+
+ {{range $value.Values}} +
{{.}}
+ {{end}} +
+
+ {{end}} + {{end}} +
+
+
+ + +
+
+ {{if .AddError}} +
+ Impossible d'ajouter la valeur. +
{{.AddError}}
+
+ {{end}} +
+ +
+ +
+
+
+
+
+
+ +{{if .Members}} +
Membres
+ +{{end}} + +{{if .Groups}} +
Membre de
+ +{{end}} + +
+ +{{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 @@ - + @@ -17,7 +17,11 @@ {{with $root := .}} {{range $user := $root.Users}} - + 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 @@
Utilisateurs Groupes - Explorateur LDAP + Explorateur LDAP
{{end}} -- cgit v1.2.3
{{ .UserNameAttr }}Identifiant Nom complet Email
{{$user.GetAttributeValue $root.UserNameAttr}} + + {{$user.GetAttributeValue $root.UserNameAttr}} + + {{$user.GetAttributeValue "displayname"}} {{$user.GetAttributeValue "mail"}}