From 670123df38608c98eadc482b9778ddfffe8560c7 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 8 Feb 2023 13:11:43 +0100 Subject: [PATCH] First iteration on mailing list administration interface --- .envrc | 1 + .gitignore | 1 + admin.go | 200 ++++++++++++++++++++++++++++-- flake.nix | 71 +++++------ invite.go | 2 +- main.go | 14 ++- profile.go | 2 +- templates/admin_groups.html | 5 + templates/admin_mailing.html | 32 +++++ templates/admin_mailing_list.html | 73 +++++++++++ templates/home.html | 1 + templates/layout.html | 2 +- 12 files changed, 348 insertions(+), 56 deletions(-) create mode 100644 .envrc create mode 100644 templates/admin_mailing.html create mode 100644 templates/admin_mailing_list.html diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 38620a8..e1a6d79 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ guichet guichet.static config.json result +.direnv/ diff --git a/admin.go b/admin.go index 832a815..b057d13 100644 --- a/admin.go +++ b/admin.go @@ -117,15 +117,186 @@ func handleAdminGroups(w http.ResponseWriter, r *http.Request) { templateAdminGroups.Execute(w, data) } +type AdminMailingTplData struct { + Login *LoginStatus + MailingNameAttr string + MailingBaseDN string + MailingLists EntryList +} + +func handleAdminMailing(w http.ResponseWriter, r *http.Request) { + templateAdminMailing := getTemplate("admin_mailing.html") + + login := checkAdminLogin(w, r) + if login == nil { + return + } + + searchRequest := ldap.NewSearchRequest( + config.MailingBaseDN, + ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=groupOfNames))"), + []string{config.MailingNameAttr, "dn", "description"}, + nil) + + sr, err := login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := &AdminMailingTplData{ + Login: login, + MailingNameAttr: config.MailingNameAttr, + MailingBaseDN: config.MailingBaseDN, + MailingLists: EntryList(sr.Entries), + } + sort.Sort(data.MailingLists) + + templateAdminMailing.Execute(w, data) +} + +type AdminMailingListTplData struct { + Login *LoginStatus + MailingNameAttr string + MailingBaseDN string + + MailingList *ldap.Entry + Members EntryList + PossibleNewMembers EntryList + + Error string + Success bool +} + +func handleAdminMailingList(w http.ResponseWriter, r *http.Request) { + templateAdminMailingList := getTemplate("admin_mailing_list.html") + + login := checkAdminLogin(w, r) + if login == nil { + return + } + + id := mux.Vars(r)["id"] + dn := fmt.Sprintf("%s=%s,%s", config.MailingNameAttr, id, config.MailingBaseDN) + + // handle modifications + dError := "" + dSuccess := false + + if r.Method == "POST" { + r.ParseForm() + action := strings.Join(r.Form["action"], "") + if action == "add-member" { + member := strings.Join(r.Form["member"], "") + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Add("member", []string{member}) + + err := login.conn.Modify(modify_request) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else if action == "delete-member" { + member := strings.Join(r.Form["member"], "") + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Delete("member", []string{member}) + + err := login.conn.Modify(modify_request) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } + } + + // Retrieve mailing list + searchRequest := ldap.NewSearchRequest( + dn, + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectclass=groupOfNames)"), + []string{"dn", config.MailingNameAttr, "member"}, + 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("Object not found: %s", dn), http.StatusNotFound) + return + } + + ml := sr.Entries[0] + + memberDns := make(map[string]bool) + for _, attr := range ml.Attributes { + if attr.Name == "member" { + for _, v := range attr.Values { + memberDns[v] = true + } + } + } + + // Retrieve list of current and possible new members + members := []*ldap.Entry{} + possibleNewMembers := []*ldap.Entry{} + + searchRequest = ldap.NewSearchRequest( + config.UserBaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectClass=organizationalPerson)"), + []string{"dn", "displayname", "mail"}, + nil) + sr, err = login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for _, ent := range sr.Entries { + if _, ok := memberDns[ent.DN]; ok { + members = append(members, ent) + } else { + possibleNewMembers = append(possibleNewMembers, ent) + } + } + + data := &AdminMailingListTplData{ + Login: login, + MailingNameAttr: config.MailingNameAttr, + MailingBaseDN: config.MailingBaseDN, + + MailingList: ml, + Members: members, + PossibleNewMembers: possibleNewMembers, + + Error: dError, + Success: dSuccess, + } + sort.Sort(data.Members) + sort.Sort(data.PossibleNewMembers) + + templateAdminMailingList.Execute(w, data) +} + +// =================================================== +// LDAP EXPLORER +// =================================================== + type AdminLDAPTplData struct { DN string - Path []PathItem - ChildrenOU []Child - ChildrenOther []Child - CanAddChild bool - Props map[string]*PropValues - CanDelete bool + Path []PathItem + ChildrenOU []Child + ChildrenOther []Child + CanAddChild bool + Props map[string]*PropValues + CanDelete bool HasMembers bool Members []EntryName @@ -523,12 +694,12 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { templateAdminLDAP.Execute(w, &AdminLDAPTplData{ DN: dn, - Path: path, + Path: path, ChildrenOU: childrenOU, - ChildrenOther: childrenOther, - Props: props, - CanAddChild: dn_last_attr == "ou" || isOrganization, - CanDelete: dn != config.BaseDN && len(childrenOU) == 0 && len(childrenOther) == 0, + ChildrenOther: childrenOther, + Props: props, + CanAddChild: dn_last_attr == "ou" || isOrganization, + CanDelete: dn != config.BaseDN && len(childrenOU) == 0 && len(childrenOther) == 0, HasMembers: len(members) > 0 || hasMembers, Members: members, @@ -671,9 +842,12 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) { if err != nil { data.Error = err.Error() } else { - http.Redirect(w, r, "/admin/ldap/"+dn, http.StatusFound) + if super_dn == config.MailingBaseDN && data.IdType == config.MailingNameAttr { + http.Redirect(w, r, "/admin/mailing/"+data.IdValue, http.StatusFound) + } else { + http.Redirect(w, r, "/admin/ldap/"+dn, http.StatusFound) + } } - } } diff --git a/flake.nix b/flake.nix index 5e2c174..7087410 100644 --- a/flake.nix +++ b/flake.nix @@ -1,43 +1,44 @@ { description = "A simple LDAP web interface for Bottin"; - inputs.nixpkgs.url = "github:nixos/nixpkgs/0244e143dc943bcf661fdaf581f01eb0f5000fcf"; - inputs.gomod2nix.url = "github:tweag/gomod2nix/40d32f82fc60d66402eb0972e6e368aeab3faf58"; + inputs.nixpkgs.url = + "github:nixos/nixpkgs/0244e143dc943bcf661fdaf581f01eb0f5000fcf"; + inputs.gomod2nix.url = + "github:tweag/gomod2nix/40d32f82fc60d66402eb0972e6e368aeab3faf58"; outputs = { self, nixpkgs, gomod2nix }: - let - pkgs = import nixpkgs { - system = "x86_64-linux"; - overlays = [ - (self: super: { - gomod = super.callPackage "${gomod2nix}/builder/" { }; - }) - ]; - }; - src = ./.; - bottin = pkgs.gomod.buildGoApplication { - pname = "guichet"; - version = "0.1.0"; - src = src; - modules = ./gomod2nix.toml; - - CGO_ENABLED=0; - - ldflags = [ - "-X main.templatePath=${src + "/templates"}" - "-X main.staticPath=${src + "/static"}" - ]; - - meta = with pkgs.lib; { - description = "A simple LDAP web interface for Bottin"; - homepage = "https://git.deuxfleurs.fr/Deuxfleurs/guichet"; - license = licenses.gpl3Plus; - platforms = platforms.linux; + let + pkgs = import nixpkgs { + system = "x86_64-linux"; + overlays = [ + (self: super: { + gomod = super.callPackage "${gomod2nix}/builder/" { }; + }) + ]; }; + src = ./.; + bottin = pkgs.gomod.buildGoApplication { + pname = "guichet"; + version = "0.1.0"; + src = src; + modules = ./gomod2nix.toml; + + CGO_ENABLED = 0; + + ldflags = [ + "-X main.templatePath=${src + "/templates"}" + "-X main.staticPath=${src + "/static"}" + ]; + + meta = with pkgs.lib; { + description = "A simple LDAP web interface for Bottin"; + homepage = "https://git.deuxfleurs.fr/Deuxfleurs/guichet"; + license = licenses.gpl3Plus; + platforms = platforms.linux; + }; + }; + in { + packages.x86_64-linux.bottin = bottin; + devShell.x86_64-linux = pkgs.mkShell { nativeBuildInputs = [ pkgs.go ]; }; }; - in - { - packages.x86_64-linux.bottin = bottin; - packages.x86_64-linux.default = self.packages.x86_64-linux.bottin; - }; } diff --git a/invite.go b/invite.go index ea356bb..1384d70 100644 --- a/invite.go +++ b/invite.go @@ -174,7 +174,7 @@ func tryCreateAccount(l *ldap.Conn, data *NewAccountData, pass1 string, pass2 st if checkFailed { return - } + } // Actually create user req := ldap.NewAddRequest(userDn, nil) diff --git a/main.go b/main.go index 137b81c..5577784 100644 --- a/main.go +++ b/main.go @@ -23,11 +23,13 @@ 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"` - GroupNameAttr string `json:"group_name_attr"` + BaseDN string `json:"base_dn"` + UserBaseDN string `json:"user_base_dn"` + UserNameAttr string `json:"user_name_attr"` + GroupBaseDN string `json:"group_base_dn"` + GroupNameAttr string `json:"group_name_attr"` + MailingBaseDN string `json:"mailing_list_base_dn"` + MailingNameAttr string `json:"mailing_list_name_attr"` InvitationBaseDN string `json:"invitation_base_dn"` InvitationNameAttr string `json:"invitation_name_attr"` @@ -131,6 +133,8 @@ func main() { r.HandleFunc("/admin/users", handleAdminUsers) r.HandleFunc("/admin/groups", handleAdminGroups) + r.HandleFunc("/admin/mailing", handleAdminMailing) + r.HandleFunc("/admin/mailing/{id}", handleAdminMailingList) r.HandleFunc("/admin/ldap/{dn}", handleAdminLDAP) r.HandleFunc("/admin/create/{template}/{super_dn}", handleAdminCreate) diff --git a/profile.go b/profile.go index e93d09b..a082ad8 100644 --- a/profile.go +++ b/profile.go @@ -121,7 +121,7 @@ func handlePasswd(w http.ResponseWriter, r *http.Request) { data.NoMatchError = true } else { modify_request := ldap.NewModifyRequest(login.Info.DN, nil) - pw, err := SSHAEncode(password); + pw, err := SSHAEncode(password) if err == nil { modify_request.Replace("userpassword", []string{pw}) err := login.conn.Modify(modify_request) diff --git a/templates/admin_groups.html b/templates/admin_groups.html index f6eabfe..ece4128 100644 --- a/templates/admin_groups.html +++ b/templates/admin_groups.html @@ -8,6 +8,11 @@ Menu principal +
+ Les groupes servent uniquement à contrôler l'accès à différentes fonctionalités de Deuxfleurs. + Ce ne sont pas des mailing lists. +
+ diff --git a/templates/admin_mailing.html b/templates/admin_mailing.html new file mode 100644 index 0000000..d81545f --- /dev/null +++ b/templates/admin_mailing.html @@ -0,0 +1,32 @@ +{{define "title"}}Mailing lists |{{end}} + +{{define "body"}} + +
+

Mailing lists

+ Nouvelle mailing list + Menu principal +
+ +
Identifiant
+ + + + + + {{with $root := .}} + {{range $ml := $root.MailingLists}} + + + + + {{end}} + {{end}} + +
AdresseDescription
+ + {{$ml.GetAttributeValue $root.MailingNameAttr}} + + {{$ml.GetAttributeValue "description"}}
+ +{{end}} diff --git a/templates/admin_mailing_list.html b/templates/admin_mailing_list.html new file mode 100644 index 0000000..c5903b6 --- /dev/null +++ b/templates/admin_mailing_list.html @@ -0,0 +1,73 @@ +{{define "title"}}ML {{.MailingList.GetAttributeValue .MailingNameAttr}} |{{end}} + +{{define "body"}} + +
+

ML {{.MailingList.GetAttributeValue .MailingNameAttr}} + Vue avancée +

+ Liste des ML + Menu principal +
+ +{{if .Success}} +
Modification enregistrée.
+{{end}} +{{if .Error}} +
+ Impossible d'effectuer la modification. +
{{.Error}}
+
+{{end}} + + + + + + + + + {{with $root := .}} + {{range $member := $root.Members}} + + + + + + {{end}} + {{end}} + +
AdresseNom
+ + {{$member.GetAttributeValue "mail"}} + + {{$member.GetAttributeValue "displayname"}} +
+ + + +
+
+ +
+
Ajouter un destinataire
+
+ +
+
Utilisateur existant : +
+
+ + + {{range .PossibleNewMembers}} + {{if .GetAttributeValue "mail"}} + + {{end}} + {{end}} + +
+
+ +
+ +{{end}} diff --git a/templates/home.html b/templates/home.html index 376aefe..afa282f 100644 --- a/templates/home.html +++ b/templates/home.html @@ -40,6 +40,7 @@
diff --git a/templates/layout.html b/templates/layout.html index 5f4a315..212ce5e 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -9,7 +9,7 @@ {{template "title"}} Guichet -
+

Guichet Deuxfleurs💮💮


{{template "body" .}}