aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2020-02-14 21:58:34 +0100
committerAlex Auvolat <alex@adnab.me>2020-02-14 21:58:34 +0100
commit151a31a4250cde38d8a66f2bfc8390f8132dc552 (patch)
tree5a5b775b4657e5d0ab44ac6f84e10b55d984bba2
parent768f2de9162bbf3fd0a1005554f3fd595818f1b3 (diff)
downloadguichet-151a31a4250cde38d8a66f2bfc8390f8132dc552.tar.gz
guichet-151a31a4250cde38d8a66f2bfc8390f8132dc552.zip
Invitation mechanism with codes etc
-rw-r--r--admin.go4
-rw-r--r--go.mod2
-rw-r--r--go.sum5
-rw-r--r--invite.go194
-rw-r--r--main.go66
-rw-r--r--templates/home.html2
-rw-r--r--templates/invite_invalid_code.html12
-rw-r--r--templates/invite_mail.txt13
-rw-r--r--templates/invite_send_code.html62
9 files changed, 328 insertions, 32 deletions
diff --git a/admin.go b/admin.go
index a0c33a6..0f78bd7 100644
--- a/admin.go
+++ b/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})
diff --git a/go.mod b/go.mod
index b11c004..f0d83c8 100644
--- a/go.mod
+++ b/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
diff --git a/go.sum b/go.sum
index 8467403..b2312a3 100644
--- a/go.sum
+++ b/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=
diff --git a/invite.go b/invite.go
index d078642..991ba2a 100644
--- a/invite.go
+++ b/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
}
+// ---- 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
}
diff --git a/main.go b/main.go
index 379a889..7126826 100644
--- a/main.go
+++ b/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)
diff --git a/templates/home.html b/templates/home.html
index 57d6930..5556ba7 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -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>
diff --git a/templates/invite_invalid_code.html b/templates/invite_invalid_code.html
new file mode 100644
index 0000000..107afab
--- /dev/null
+++ b/templates/invite_invalid_code.html
@@ -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}}
diff --git a/templates/invite_mail.txt b/templates/invite_mail.txt
new file mode 100644
index 0000000..f1667b7
--- /dev/null
+++ b/templates/invite_mail.txt
@@ -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 !
+
diff --git a/templates/invite_send_code.html b/templates/invite_send_code.html
new file mode 100644
index 0000000..8d09b3a
--- /dev/null
+++ b/templates/invite_send_code.html
@@ -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}}