diff options
-rw-r--r-- | admin.go | 287 | ||||
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | go.sum | 2 | ||||
-rw-r--r-- | main.go | 24 | ||||
-rw-r--r-- | templates/admin_groups.html | 31 | ||||
-rw-r--r-- | templates/admin_ldap.html | 134 | ||||
-rw-r--r-- | templates/admin_users.html | 8 | ||||
-rw-r--r-- | templates/home.html | 2 |
8 files changed, 466 insertions, 23 deletions
@@ -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, + }) } @@ -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 ) @@ -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= @@ -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}} |