diff options
author | Alex Auvolat <alex@adnab.me> | 2023-02-08 13:11:43 +0100 |
---|---|---|
committer | Alex Auvolat <alex@adnab.me> | 2023-02-08 13:11:43 +0100 |
commit | 670123df38608c98eadc482b9778ddfffe8560c7 (patch) | |
tree | 6b18562f502100a3dc80615a1b0b7d7c5203852f | |
parent | cd415325729fddea26c816291bb33171b9cc4879 (diff) | |
download | guichet-670123df38608c98eadc482b9778ddfffe8560c7.tar.gz guichet-670123df38608c98eadc482b9778ddfffe8560c7.zip |
First iteration on mailing list administration interface
-rw-r--r-- | .envrc | 1 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | admin.go | 200 | ||||
-rw-r--r-- | flake.nix | 65 | ||||
-rw-r--r-- | invite.go | 2 | ||||
-rw-r--r-- | main.go | 14 | ||||
-rw-r--r-- | profile.go | 2 | ||||
-rw-r--r-- | templates/admin_groups.html | 5 | ||||
-rw-r--r-- | templates/admin_mailing.html | 32 | ||||
-rw-r--r-- | templates/admin_mailing_list.html | 73 | ||||
-rw-r--r-- | templates/home.html | 1 | ||||
-rw-r--r-- | templates/layout.html | 2 |
12 files changed, 345 insertions, 53 deletions
@@ -0,0 +1 @@ +use flake @@ -2,3 +2,4 @@ guichet guichet.static config.json result +.direnv/ @@ -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) + } } - } } @@ -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; + 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; + CGO_ENABLED = 0; - ldflags = [ - "-X main.templatePath=${src + "/templates"}" - "-X main.staticPath=${src + "/static"}" - ]; + 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; + 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; - }; } @@ -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) @@ -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) @@ -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 @@ <a class="ml-4 btn btn-info" href="/">Menu principal</a> </div> +<div class="alert alert-warning mt-4"> + Les groupes servent uniquement à contrôler l'accès à différentes fonctionalités de Deuxfleurs. + Ce ne sont pas des <a href="/admin/mailing">mailing lists</a>. +</div> + <table class="table mt-4"> <thead> <th scope="col">Identifiant</th> 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"}} + +<div class="d-flex"> + <h4>Mailing lists</h4> + <a class="ml-auto btn btn-success" href="/admin/create/group/{{.MailingBaseDN}}">Nouvelle mailing list</a> + <a class="ml-4 btn btn-info" href="/">Menu principal</a> +</div> + +<table class="table mt-4"> + <thead> + <th scope="col">Adresse</th> + <th scope="col">Description</th> + </thead> + <tbody> + {{with $root := .}} + {{range $ml := $root.MailingLists}} + <tr> + <td> + <a href="/admin/mailing/{{$ml.GetAttributeValue $root.MailingNameAttr}}"> + {{$ml.GetAttributeValue $root.MailingNameAttr}} + </a> + </td> + <td>{{$ml.GetAttributeValue "description"}}</td> + </tr> + {{end}} + {{end}} + </tbody> +</table> + +{{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"}} + +<div class="d-flex"> + <h4>ML {{.MailingList.GetAttributeValue .MailingNameAttr}} + <a class="ml-auto btn btn-sm btn-dark" href="/admin/ldap/{{.MailingList.DN}}">Vue avancée</a> + </h4> + <a class="ml-auto btn btn-dark" href="/admin/mailing">Liste des ML</a> + <a class="ml-4 btn btn-info" href="/">Menu principal</a> +</div> + +{{if .Success}} + <div class="alert alert-success mt-2">Modification enregistrée.</div> +{{end}} +{{if .Error}} + <div class="alert alert-danger mt-2"> + Impossible d'effectuer la modification. + <div style="font-size: 0.8em">{{.Error}}</div> + </div> +{{end}} + +<table class="table mt-4"> + <thead> + <th scope="col">Adresse</th> + <th scope="col">Nom</th> + <th scope="col" style="width: 6em"></th> + </thead> + <tbody> + {{with $root := .}} + {{range $member := $root.Members}} + <tr> + <td> + <a href="/admin/ldap/{{$member.DN}}"> + {{$member.GetAttributeValue "mail"}} + </a> + </td> + <td>{{$member.GetAttributeValue "displayname"}}</td> + <td> + <form method="POST" onsubmit="return confirm('Supprimer de la ML ?');"> + <input type="hidden" name="action" value="delete-member" /> + <input type="hidden" name="member" value="{{.DN}}" /> + <input type="submit" value="Suppr" class="form-control btn btn-danger btn-sm" /> + </form> + </td> + </tr> + {{end}} + {{end}} + </tbody> +</table> + + <hr class="mt-4" /> + <h5 class="mt-4">Ajouter un destinataire</h5> + <form method="POST"> + <input type="hidden" name="action" value="add-member" /> + <div class="row mt-4"> + <div class="col-md-3"><strong>Utilisateur existant :</strong> + </div> + <div class="col-md-5"> + <input class="form-control" type="text" list="users" name="member" placeholder="Utilisateur..." /> + <datalist id="users"> + {{range .PossibleNewMembers}} + {{if .GetAttributeValue "mail"}} + <option value="{{.DN}}">{{if .GetAttributeValue "displayname"}}{{.GetAttributeValue "displayname"}} ({{.GetAttributeValue "mail" }}){{else}}{{.GetAttributeValue "mail"}}{{end}}</option> + {{end}} + {{end}} + </datalist> + </div> + <div class="col-md-2"> + <input type="submit" value="Ajouter" class="form-control btn btn-success btn-sm" /> + </div> + </form> +{{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 @@ <div class="list-group list-group-flush"> <a class="list-group-item list-group-item-action" href="/admin/users">Utilisateur·ices</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/mailing">Mailing lists</a> <a class="list-group-item list-group-item-action" href="/admin/ldap/{{.BaseDN}}">Explorateur LDAP</a> </div> </div> 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 @@ <title>{{template "title"}} Guichet</title> </head> <body> - <div class="container"> + <div class="container mb-4"> <h1>Guichet Deuxfleurs💮💮</h1> <hr /> {{template "body" .}} |