First iteration on mailing list administration interface

This commit is contained in:
Alex Auvolat 2023-02-08 13:11:43 +01:00
parent cd41532572
commit 670123df38
12 changed files with 348 additions and 56 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ guichet
guichet.static guichet.static
config.json config.json
result result
.direnv/

200
admin.go
View File

@ -117,15 +117,186 @@ func handleAdminGroups(w http.ResponseWriter, r *http.Request) {
templateAdminGroups.Execute(w, data) 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 { type AdminLDAPTplData struct {
DN string DN string
Path []PathItem Path []PathItem
ChildrenOU []Child ChildrenOU []Child
ChildrenOther []Child ChildrenOther []Child
CanAddChild bool CanAddChild bool
Props map[string]*PropValues Props map[string]*PropValues
CanDelete bool CanDelete bool
HasMembers bool HasMembers bool
Members []EntryName Members []EntryName
@ -523,12 +694,12 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
templateAdminLDAP.Execute(w, &AdminLDAPTplData{ templateAdminLDAP.Execute(w, &AdminLDAPTplData{
DN: dn, DN: dn,
Path: path, Path: path,
ChildrenOU: childrenOU, ChildrenOU: childrenOU,
ChildrenOther: childrenOther, ChildrenOther: childrenOther,
Props: props, Props: props,
CanAddChild: dn_last_attr == "ou" || isOrganization, CanAddChild: dn_last_attr == "ou" || isOrganization,
CanDelete: dn != config.BaseDN && len(childrenOU) == 0 && len(childrenOther) == 0, CanDelete: dn != config.BaseDN && len(childrenOU) == 0 && len(childrenOther) == 0,
HasMembers: len(members) > 0 || hasMembers, HasMembers: len(members) > 0 || hasMembers,
Members: members, Members: members,
@ -671,9 +842,12 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
data.Error = err.Error() data.Error = err.Error()
} else { } 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)
}
} }
} }
} }

View File

@ -1,43 +1,44 @@
{ {
description = "A simple LDAP web interface for Bottin"; description = "A simple LDAP web interface for Bottin";
inputs.nixpkgs.url = "github:nixos/nixpkgs/0244e143dc943bcf661fdaf581f01eb0f5000fcf"; inputs.nixpkgs.url =
inputs.gomod2nix.url = "github:tweag/gomod2nix/40d32f82fc60d66402eb0972e6e368aeab3faf58"; "github:nixos/nixpkgs/0244e143dc943bcf661fdaf581f01eb0f5000fcf";
inputs.gomod2nix.url =
"github:tweag/gomod2nix/40d32f82fc60d66402eb0972e6e368aeab3faf58";
outputs = { self, nixpkgs, gomod2nix }: outputs = { self, nixpkgs, gomod2nix }:
let let
pkgs = import nixpkgs { pkgs = import nixpkgs {
system = "x86_64-linux"; system = "x86_64-linux";
overlays = [ overlays = [
(self: super: { (self: super: {
gomod = super.callPackage "${gomod2nix}/builder/" { }; 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;
}; };
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;
};
} }

View File

@ -174,7 +174,7 @@ func tryCreateAccount(l *ldap.Conn, data *NewAccountData, pass1 string, pass2 st
if checkFailed { if checkFailed {
return return
} }
// Actually create user // Actually create user
req := ldap.NewAddRequest(userDn, nil) req := ldap.NewAddRequest(userDn, nil)

14
main.go
View File

@ -23,11 +23,13 @@ type ConfigFile struct {
LdapServerAddr string `json:"ldap_server_addr"` LdapServerAddr string `json:"ldap_server_addr"`
LdapTLS bool `json:"ldap_tls"` LdapTLS bool `json:"ldap_tls"`
BaseDN string `json:"base_dn"` BaseDN string `json:"base_dn"`
UserBaseDN string `json:"user_base_dn"` UserBaseDN string `json:"user_base_dn"`
UserNameAttr string `json:"user_name_attr"` UserNameAttr string `json:"user_name_attr"`
GroupBaseDN string `json:"group_base_dn"` GroupBaseDN string `json:"group_base_dn"`
GroupNameAttr string `json:"group_name_attr"` 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"` InvitationBaseDN string `json:"invitation_base_dn"`
InvitationNameAttr string `json:"invitation_name_attr"` InvitationNameAttr string `json:"invitation_name_attr"`
@ -131,6 +133,8 @@ func main() {
r.HandleFunc("/admin/users", handleAdminUsers) r.HandleFunc("/admin/users", handleAdminUsers)
r.HandleFunc("/admin/groups", handleAdminGroups) 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/ldap/{dn}", handleAdminLDAP)
r.HandleFunc("/admin/create/{template}/{super_dn}", handleAdminCreate) r.HandleFunc("/admin/create/{template}/{super_dn}", handleAdminCreate)

View File

@ -121,7 +121,7 @@ func handlePasswd(w http.ResponseWriter, r *http.Request) {
data.NoMatchError = true data.NoMatchError = true
} else { } else {
modify_request := ldap.NewModifyRequest(login.Info.DN, nil) modify_request := ldap.NewModifyRequest(login.Info.DN, nil)
pw, err := SSHAEncode(password); pw, err := SSHAEncode(password)
if err == nil { if err == nil {
modify_request.Replace("userpassword", []string{pw}) modify_request.Replace("userpassword", []string{pw})
err := login.conn.Modify(modify_request) err := login.conn.Modify(modify_request)

View File

@ -8,6 +8,11 @@
<a class="ml-4 btn btn-info" href="/">Menu principal</a> <a class="ml-4 btn btn-info" href="/">Menu principal</a>
</div> </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"> <table class="table mt-4">
<thead> <thead>
<th scope="col">Identifiant</th> <th scope="col">Identifiant</th>

View File

@ -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}}

View File

@ -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}}

View File

@ -40,6 +40,7 @@
<div class="list-group list-group-flush"> <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/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/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> <a class="list-group-item list-group-item-action" href="/admin/ldap/{{.BaseDN}}">Explorateur LDAP</a>
</div> </div>
</div> </div>

View File

@ -9,7 +9,7 @@
<title>{{template "title"}} Guichet</title> <title>{{template "title"}} Guichet</title>
</head> </head>
<body> <body>
<div class="container"> <div class="container mb-4">
<h1>Guichet Deuxfleurs💮💮</h1> <h1>Guichet Deuxfleurs💮💮</h1>
<hr /> <hr />
{{template "body" .}} {{template "body" .}}