Invitation mechanism with codes etc
This commit is contained in:
parent
768f2de916
commit
151a31a425
9 changed files with 329 additions and 33 deletions
4
admin.go
4
admin.go
|
@ -600,9 +600,9 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
|
|||
} else {
|
||||
dn := data.IdType + "=" + data.IdValue + "," + super_dn
|
||||
req := ldap.NewAddRequest(dn, nil)
|
||||
req.Attribute("objectClass", object_class)
|
||||
req.Attribute("objectclass", object_class)
|
||||
if data.StructuralObjectClass != "" {
|
||||
req.Attribute("structuralObjectClass", []string{data.StructuralObjectClass})
|
||||
req.Attribute("structuralobjectclass", []string{data.StructuralObjectClass})
|
||||
}
|
||||
if data.DisplayName != "" {
|
||||
req.Attribute("displayname", []string{data.DisplayName})
|
||||
|
|
2
go.mod
2
go.mod
|
@ -3,6 +3,8 @@ module deuxfleurs.fr/Deuxfleurs/guichet
|
|||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
|
||||
github.com/emersion/go-smtp v0.12.1
|
||||
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
|
||||
|
|
5
go.sum
5
go.sum
|
@ -1,4 +1,9 @@
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||
github.com/emersion/go-smtp v0.12.1 h1:1R8BDqrR2HhlGwgFYcOi+BVTvK1bMjAB65QcVpJ5sNA=
|
||||
github.com/emersion/go-smtp v0.12.1/go.mod h1:SD9V/xa4ndMw77lR3Mf7htkp8RBNYuPh9UeuBs9tpUQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk=
|
||||
|
|
196
invite.go
196
invite.go
|
@ -1,15 +1,26 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
var EMAIL_REGEXP = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||
|
||||
func checkInviterLogin(w http.ResponseWriter, r *http.Request) *LoginStatus {
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
|
@ -24,6 +35,47 @@ func checkInviterLogin(w http.ResponseWriter, r *http.Request) *LoginStatus {
|
|||
return login
|
||||
}
|
||||
|
||||
// New account creation directly from interface
|
||||
|
||||
func handleInviteNewAccount(w http.ResponseWriter, r *http.Request) {
|
||||
login := checkInviterLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
handleNewAccount(w, r, login.conn)
|
||||
}
|
||||
|
||||
// New account creation using code
|
||||
|
||||
func handleInvitationCode(w http.ResponseWriter, r *http.Request) {
|
||||
code := mux.Vars(r)["code"]
|
||||
code_id, code_pw := readCode(code)
|
||||
|
||||
l := ldapOpen(w)
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
|
||||
inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN
|
||||
err := l.Bind(inviteDn, code_pw)
|
||||
if err != nil {
|
||||
templateInviteInvalidCode := template.Must(template.ParseFiles("templates/layout.html", "templates/invite_invalid_code.html"))
|
||||
templateInviteInvalidCode.Execute(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if handleNewAccount(w, r, l) {
|
||||
del_req := ldap.NewDelRequest(inviteDn, nil)
|
||||
err = l.Del(del_req)
|
||||
if err != nil {
|
||||
log.Printf("Could not delete invitation %s: %s", inviteDn, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Common functions for new account
|
||||
|
||||
type NewAccountData struct {
|
||||
Username string
|
||||
DisplayName string
|
||||
|
@ -39,14 +91,9 @@ type NewAccountData struct {
|
|||
Success bool
|
||||
}
|
||||
|
||||
func handleInviteNewAccount(w http.ResponseWriter, r *http.Request) {
|
||||
func handleNewAccount(w http.ResponseWriter, r *http.Request, l *ldap.Conn) bool {
|
||||
templateInviteNewAccount := template.Must(template.ParseFiles("templates/layout.html", "templates/invite_new_account.html"))
|
||||
|
||||
login := checkInviterLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := &NewAccountData{}
|
||||
|
||||
if r.Method == "POST" {
|
||||
|
@ -60,10 +107,11 @@ func handleInviteNewAccount(w http.ResponseWriter, r *http.Request) {
|
|||
password1 := strings.Join(r.Form["password"], "")
|
||||
password2 := strings.Join(r.Form["password2"], "")
|
||||
|
||||
tryCreateAccount(login.conn, data, password1, password2)
|
||||
tryCreateAccount(l, data, password1, password2)
|
||||
}
|
||||
|
||||
templateInviteNewAccount.Execute(w, data)
|
||||
return data.Success
|
||||
}
|
||||
|
||||
func tryCreateAccount(l *ldap.Conn, data *NewAccountData, pass1 string, pass2 string) {
|
||||
|
@ -140,5 +188,137 @@ func tryCreateAccount(l *ldap.Conn, data *NewAccountData, pass1 string, pass2 st
|
|||
data.Success = true
|
||||
}
|
||||
|
||||
func handleInviteSendCode(w http.ResponseWriter, r *http.Request) {
|
||||
// ---- Code generation ----
|
||||
|
||||
type SendCodeData struct {
|
||||
ErrorMessage string
|
||||
ErrorInvalidEmail bool
|
||||
Success bool
|
||||
CodeDisplay string
|
||||
CodeSentTo string
|
||||
WebBaseAddress string
|
||||
}
|
||||
|
||||
type CodeMailFields struct {
|
||||
From string
|
||||
To string
|
||||
Code string
|
||||
InviteFrom string
|
||||
WebBaseAddress string
|
||||
}
|
||||
|
||||
func handleInviteSendCode(w http.ResponseWriter, r *http.Request) {
|
||||
templateInviteSendCode := template.Must(template.ParseFiles("templates/layout.html", "templates/invite_send_code.html"))
|
||||
|
||||
login := checkInviterLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := &SendCodeData{
|
||||
WebBaseAddress: config.WebAddress,
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
r.ParseForm()
|
||||
|
||||
choice := strings.Join(r.Form["choice"], "")
|
||||
if choice != "display" && choice != "send" {
|
||||
http.Error(w, "Invalid entry", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sendto := strings.Join(r.Form["sendto"], "")
|
||||
|
||||
trySendCode(login, choice, sendto, data)
|
||||
}
|
||||
|
||||
templateInviteSendCode.Execute(w, data)
|
||||
}
|
||||
|
||||
func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCodeData) {
|
||||
// Generate code
|
||||
code, code_id, code_pw := genCode()
|
||||
|
||||
// Create invitation object in database
|
||||
inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN
|
||||
req := ldap.NewAddRequest(inviteDn, nil)
|
||||
req.Attribute("userpassword", []string{SSHAEncode([]byte(code_pw))})
|
||||
req.Attribute("objectclass", []string{"top", "invitationCode"})
|
||||
|
||||
err := login.conn.Add(req)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
// If we want to display it, do so
|
||||
if choice == "display" {
|
||||
data.Success = true
|
||||
data.CodeDisplay = code
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, we are sending a mail
|
||||
if !EMAIL_REGEXP.MatchString(sendto) {
|
||||
data.ErrorInvalidEmail = true
|
||||
return
|
||||
}
|
||||
|
||||
templateMail := template.Must(template.ParseFiles("templates/invite_mail.txt"))
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
templateMail.Execute(buf, &CodeMailFields{
|
||||
To: sendto,
|
||||
From: config.MailFrom,
|
||||
InviteFrom: login.WelcomeName(),
|
||||
Code: code,
|
||||
WebBaseAddress: config.WebAddress,
|
||||
})
|
||||
|
||||
log.Printf("Sending mail to: %s", sendto)
|
||||
var auth sasl.Client = nil
|
||||
if config.SMTPUsername != "" {
|
||||
auth = sasl.NewPlainClient("", config.SMTPUsername, config.SMTPPassword)
|
||||
}
|
||||
err = smtp.SendMail(config.SMTPServer, auth, config.MailFrom, []string{sendto}, buf)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
return
|
||||
}
|
||||
log.Printf("Mail sent.")
|
||||
|
||||
data.Success = true
|
||||
data.CodeSentTo = sendto
|
||||
}
|
||||
|
||||
func genCode() (code string, code_id string, code_pw string) {
|
||||
random := make([]byte, 32)
|
||||
n, err := rand.Read(random)
|
||||
if err != nil || n != 32 {
|
||||
log.Fatalf("Could not generate random bytes: %s", err)
|
||||
}
|
||||
|
||||
a := binary.BigEndian.Uint32(random[0:4])
|
||||
b := binary.BigEndian.Uint32(random[4:8])
|
||||
c := binary.BigEndian.Uint32(random[8:12])
|
||||
|
||||
code = fmt.Sprintf("%03d-%03d-%03d", a%1000, b%1000, c%1000)
|
||||
code_id, code_pw = readCode(code)
|
||||
return
|
||||
}
|
||||
|
||||
func readCode(code string) (code_id string, code_pw string) {
|
||||
// Strip everything that is not a digit
|
||||
code_digits := ""
|
||||
for _, c := range code {
|
||||
if c >= '0' && c <= '9' {
|
||||
code_digits = code_digits + string(c)
|
||||
}
|
||||
}
|
||||
|
||||
id_hash := sha256.Sum256([]byte("Guichet ID " + code_digits))
|
||||
pw_hash := sha256.Sum256([]byte("Guichet PW " + code_digits))
|
||||
|
||||
code_id = hex.EncodeToString(id_hash[:8])
|
||||
code_pw = hex.EncodeToString(pw_hash[:16])
|
||||
return
|
||||
}
|
||||
|
|
66
main.go
66
main.go
|
@ -23,16 +23,23 @@ 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"`
|
||||
|
||||
InvitationBaseDN string `json:"invitation_base_dn"`
|
||||
InvitationNameAttr string `json:"invitation_name_attr"`
|
||||
InvitedMailFormat string `json:"invited_mail_format"`
|
||||
InvitedAutoGroups []string `json:"invited_auto_groups"`
|
||||
|
||||
WebAddress string `json:"web_address"`
|
||||
MailFrom string `json:"mail_from"`
|
||||
SMTPServer string `json:"smtp_server"`
|
||||
SMTPUsername string `json:"smtp_username"`
|
||||
SMTPPassword string `json:"smtp_password"`
|
||||
|
||||
AdminAccount string `json:"admin_account"`
|
||||
GroupCanInvite string `json:"group_can_invite"`
|
||||
GroupCanAdmin string `json:"group_can_admin"`
|
||||
|
@ -51,11 +58,22 @@ func readConfig() ConfigFile {
|
|||
HttpBindAddr: ":9991",
|
||||
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",
|
||||
GroupNameAttr: "gid",
|
||||
|
||||
BaseDN: "dc=example,dc=com",
|
||||
UserBaseDN: "ou=users,dc=example,dc=com",
|
||||
UserNameAttr: "uid",
|
||||
GroupBaseDN: "ou=groups,dc=example,dc=com",
|
||||
GroupNameAttr: "gid",
|
||||
|
||||
InvitationBaseDN: "ou=invitations,dc=example,dc=com",
|
||||
InvitationNameAttr: "cn",
|
||||
InvitedMailFormat: "{}@example.com",
|
||||
InvitedAutoGroups: []string{},
|
||||
|
||||
WebAddress: "https://guichet.example.com",
|
||||
MailFrom: "guichet@example.com",
|
||||
SMTPServer: "smtp.example.com",
|
||||
|
||||
AdminAccount: "uid=admin,dc=example,dc=com",
|
||||
GroupCanInvite: "",
|
||||
GroupCanAdmin: "gid=admin,ou=groups,dc=example,dc=com",
|
||||
|
@ -117,6 +135,7 @@ func main() {
|
|||
|
||||
r.HandleFunc("/invite/new_account", handleInviteNewAccount)
|
||||
r.HandleFunc("/invite/send_code", handleInviteSendCode)
|
||||
r.HandleFunc("/invitation/{code}", handleInvitationCode)
|
||||
|
||||
r.HandleFunc("/admin/users", handleAdminUsers)
|
||||
r.HandleFunc("/admin/groups", handleAdminGroups)
|
||||
|
@ -147,6 +166,17 @@ type LoginStatus struct {
|
|||
CanInvite bool
|
||||
}
|
||||
|
||||
func (login *LoginStatus) WelcomeName() string {
|
||||
ret := login.UserEntry.GetAttributeValue("givenname")
|
||||
if ret == "" {
|
||||
ret = login.UserEntry.GetAttributeValue("displayname")
|
||||
}
|
||||
if ret == "" {
|
||||
ret = login.Info.Username
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func logRequest(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL)
|
||||
|
@ -266,9 +296,8 @@ func ldapOpen(w http.ResponseWriter) *ldap.Conn {
|
|||
// Page handlers ----
|
||||
|
||||
type HomePageData struct {
|
||||
Login *LoginStatus
|
||||
WelcomeName string
|
||||
BaseDN string
|
||||
Login *LoginStatus
|
||||
BaseDN string
|
||||
}
|
||||
|
||||
func handleHome(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -280,15 +309,8 @@ func handleHome(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
data := &HomePageData{
|
||||
Login: login,
|
||||
BaseDN: config.BaseDN,
|
||||
WelcomeName: login.UserEntry.GetAttributeValue("givenname"),
|
||||
}
|
||||
if data.WelcomeName == "" {
|
||||
data.WelcomeName = login.UserEntry.GetAttributeValue("displayname")
|
||||
}
|
||||
if data.WelcomeName == "" {
|
||||
data.WelcomeName = login.Info.Username
|
||||
Login: login,
|
||||
BaseDN: config.BaseDN,
|
||||
}
|
||||
|
||||
templateHome.Execute(w, data)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{{define "body"}}
|
||||
<div class="alert alert-info">
|
||||
Bienvenue, <strong>{{ .WelcomeName }}</strong> !
|
||||
Bienvenue, <strong>{{ .Login.WelcomeName }}</strong> !
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a class="ml-auto btn btn-sm btn-dark" href="/logout">Se déconnecter</a>
|
||||
|
|
12
templates/invite_invalid_code.html
Normal file
12
templates/invite_invalid_code.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{{define "title"}}Créer un compte |{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="d-flex">
|
||||
<h4>Création d'un nouveau compte</h4>
|
||||
<a class="ml-auto btn btn-info" href="/">Retour</a>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger mt-4">
|
||||
Code d'invitation invalide.
|
||||
</div>
|
||||
{{end}}
|
13
templates/invite_mail.txt
Normal file
13
templates/invite_mail.txt
Normal file
|
@ -0,0 +1,13 @@
|
|||
From: {{.From}}
|
||||
To: {{.To}}
|
||||
Subject: Code d'invitation Deuxfleurs
|
||||
Content-type: text/plain; charset=utf-8
|
||||
|
||||
Vous avez été invité à créer un compte sur Deuxfleurs par {{.InviteFrom}} :)
|
||||
|
||||
Pour créer votre compte, rendez-vous à l'addresse suivante:
|
||||
|
||||
{{.WebBaseAddress}}/invite/{{.Code}}
|
||||
|
||||
À bientôt sur Deuxfleurs !
|
||||
|
62
templates/invite_send_code.html
Normal file
62
templates/invite_send_code.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
{{define "title"}}Envoyer un code d'invitation |{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="d-flex">
|
||||
<h4>Envoyer un code d'invitation</h4>
|
||||
<a class="ml-auto btn btn-info" href="/">Retour</a>
|
||||
</div>
|
||||
|
||||
{{if .ErrorMessage}}
|
||||
<div class="alert alert-danger mt-4">Impossible de génerer ou d'envoyer le code.
|
||||
<div style="font-size: 0.8em">{{ .ErrorMessage }}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Success}}
|
||||
<div class="alert alert-success mt-4">
|
||||
{{if .CodeSentTo}}
|
||||
Un code d'invitation a bien été envoyé à <code>{{ .CodeSentTo }}</code>.
|
||||
{{end}}
|
||||
{{if .CodeDisplay}}
|
||||
Le code généré est le suivant:
|
||||
|
||||
<p style="text-align: center; font-size: 1.6em;" class="mt-4 mb-4">
|
||||
{{ .CodeDisplay }}
|
||||
</p>
|
||||
<p style="text-align: center" class="mt-4 mb-4">
|
||||
<a href="{{.WebBaseAddress}}/invitation/{{ .CodeDisplay }}">{{.WebBaseAddress}}/invitation/{{.CodeDisplay}}</a>
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<form method="POST" class="mt-4">
|
||||
Choisissez une option:
|
||||
|
||||
<div class="input-group mt-4">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<input type="radio" name="choice" value="display" id="choice_display" checked="true">
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-control" for="choice_display">
|
||||
Afficher le code et me laisser l'envoyer
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-group mt-4">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<input type="radio" name="choice" value="send" id="choice_send" aria-label="Checkbox for following text input">
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-control" for="choice_send">
|
||||
Envoyer le code à l'addresse suivante:
|
||||
</label>
|
||||
<input class="form-control" type="text" name="sendto" id="sendto" placeholder="Addresse mail..." onclick="document.getElementById('choice_send').checked = true;" />
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-4">
|
||||
<button type="submit" class="btn btn-primary">Génerer le code</button>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
{{end}}
|
Loading…
Reference in a new issue