guichet/invite.go

614 lines
17 KiB
Go

package main
import (
"bytes"
"crypto/rand"
"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"
"golang.org/x/crypto/argon2"
)
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 {
return nil
}
// if !login.CanInvite {
// http.Error(w, "Not authorized to invite new users.", http.StatusUnauthorized)
// return nil
// }
return login
}
// New account creation directly from interface
type PasswordFoundData struct {
ErrorMessage string
Success bool
Username string
Mail string
OtherMailbox string
CanAdmin bool
LoggedIn bool
}
type PasswordLostData struct {
ErrorMessage string
Success bool
Username string
Mail string
OtherMailbox string
CanAdmin bool
LoggedIn bool
}
func openNewUserLdap(config *ConfigFile) (*ldap.Conn, error) {
l, err := openLdap(config)
if err != nil {
log.Printf(fmt.Sprintf("openNewUserLdap 1 : %v %v", err, l))
log.Printf(fmt.Sprintf("openNewUserLdap 1 : %v", config))
// data.ErrorMessage = err.Error()
}
err = l.Bind(config.NewUserDN, config.NewUserPassword)
if err != nil {
log.Printf(fmt.Sprintf("openNewUserLdap 2 : %v", err))
log.Printf(fmt.Sprintf("openNewUserLdap 2 : %v", config.NewUserDN))
log.Printf(fmt.Sprintf("openNewUserLdap 2 : %v", config.NewUserPassword))
log.Printf(fmt.Sprintf("openNewUserLdap 2 : %v", config))
// data.ErrorMessage = err.Error()
}
return l, err
}
func handleLostPassword(w http.ResponseWriter, r *http.Request) {
templateLostPasswordPage := getTemplate("password_lost.html")
if checkLogin(w, r) != nil {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
data := PasswordLostData{
CanAdmin: false,
LoggedIn: false,
}
if r.Method == "POST" {
r.ParseForm()
data.Username = strings.TrimSpace(strings.Join(r.Form["username"], ""))
data.Mail = strings.TrimSpace(strings.Join(r.Form["mail"], ""))
data.OtherMailbox = strings.TrimSpace(strings.Join(r.Form["othermailbox"], ""))
user := User{
CN: strings.TrimSpace(strings.Join(r.Form["username"], "")),
UID: strings.TrimSpace(strings.Join(r.Form["username"], "")),
Mail: strings.TrimSpace(strings.Join(r.Form["mail"], "")),
OtherMailbox: strings.TrimSpace(strings.Join(r.Form["othermailbox"], "")),
}
ldapConn, err := openNewUserLdap(config)
if err != nil {
log.Printf(fmt.Sprintf("handleLostPassword 99 : %v %v", err, ldapConn))
data.ErrorMessage = err.Error()
}
err = passwordLost(user, config, ldapConn)
if err != nil {
log.Printf(fmt.Sprintf("handleLostPassword 104 : %v %v", err, ldapConn))
data.ErrorMessage = err.Error()
} else {
err = ldapConn.Bind(config.NewUserDN, config.NewUserPassword)
if err != nil {
log.Printf(fmt.Sprintf("handleLostPassword 109 : %v %v", err, ldapConn))
data.ErrorMessage = err.Error()
} else {
data.Success = true
}
}
}
data.CanAdmin = false
templateLostPasswordPage.Execute(w, data)
}
func handleInviteNewAccount(w http.ResponseWriter, r *http.Request) {
l, err := ldapOpen(w)
if err != nil {
log.Printf(fmt.Sprintf("58: %v %v", err, l))
}
// l.Bind(config.NewUserDN, config.NewUserPassword)
// login := checkInviterLogin(w, r)
// if login == nil {
// return
// }
// l, _ := ldap.DialURL(config.LdapServerAddr)
// l.Bind(config.NewUserDN, config.NewUserPassword)
// loginInfo, err := doLogin(w, r, "testuser", config.NewUserDN, config.NewUserPassword)
// if err != nil {
// log.Printf(fmt.Sprintf("58: %v %v", err, l))
// }
// l := ldapOpen(w)
if l == nil {
return
}
err = l.Bind(config.NewUserDN, config.NewUserPassword)
if err != nil {
log.Printf(fmt.Sprintf("58: %v %v", err, l))
}
handleNewAccount(w, r, l, config.NewUserDN)
}
// New account creation using code
func handleInvitationCode(w http.ResponseWriter, r *http.Request) {
code := mux.Vars(r)["code"]
code_id, code_pw := readCode(code)
// log.Printf(code_pw)
login := checkLogin(w, r)
// l := ldapOpen(w)
// if l == nil {
// return
// }
inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN
err := login.conn.Bind(inviteDn, code_pw)
if err != nil {
templateInviteInvalidCode := getTemplate("invite_invalid_code.html")
templateInviteInvalidCode.Execute(w, nil)
return
}
sReq := ldap.NewSearchRequest(
inviteDn,
ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(objectclass=*)"),
[]string{"dn", "creatorsname"},
nil)
sr, err := login.conn.Search(sReq)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(sr.Entries) != 1 {
http.Error(w, fmt.Sprintf("Expected 1 entry, got %d", len(sr.Entries)), http.StatusInternalServerError)
return
}
invitedBy := sr.Entries[0].GetAttributeValue("creatorsname")
if handleNewAccount(w, r, login.conn, invitedBy) {
del_req := ldap.NewDelRequest(inviteDn, nil)
err = login.conn.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
GivenName string
Surname string
Mail string
SuggestPW string
OtherEmail string
ErrorUsernameTaken bool
ErrorInvalidUsername bool
ErrorPasswordTooShort bool
ErrorPasswordMismatch bool
ErrorMessage string
WarningMessage string
Success bool
CanAdmin bool
LoggedIn bool
}
func handleNewAccount(w http.ResponseWriter, r *http.Request, l *ldap.Conn, invitedBy string) bool {
templateInviteNewAccount := getTemplate("invite_new_account.html")
data := &NewAccountData{}
if r.Method == "POST" {
r.ParseForm()
newUser := User{}
// login := checkLogin(w, r)
// newUser.Mail = fmt.Sprintf("%s@%s", strings.TrimSpace(strings.Join(r.Form["username"], "")), "lesgv.com")
newUser.DisplayName = strings.TrimSpace(strings.Join(r.Form["displayname"], ""))
newUser.GivenName = strings.TrimSpace(strings.Join(r.Form["givenname"], ""))
newUser.SN = strings.TrimSpace(strings.Join(r.Form["surname"], ""))
newUser.Mail = strings.TrimSpace(strings.Join(r.Form["mail"], ""))
newUser.UID = strings.TrimSpace(strings.Join(r.Form["otheremail"], ""))
newUser.CN = strings.TrimSpace(strings.Join(r.Form["username"], ""))
newUser.DN = "cn=" + strings.TrimSpace(strings.Join(r.Form["username"], "")) + "," + config.InvitationBaseDN
password1 := strings.Join(r.Form["password"], "")
password2 := strings.Join(r.Form["password2"], "")
if password1 != password2 {
data.Success = false
data.ErrorPasswordMismatch = true
} else {
newUser.Password = password2
l.Bind(config.NewUserDN, config.NewUserPassword)
err := add(newUser, config, l)
if err != nil {
data.Success = false
data.ErrorMessage = err.Error()
}
http.Redirect(w, r, "/admin/activate", http.StatusFound)
}
// tryCreateAccount(l, data, password1, password2, invitedBy)
} else {
data.SuggestPW = fmt.Sprintf("%s", suggestPassword())
}
data.CanAdmin = false
data.LoggedIn = false
templateInviteNewAccount.Execute(w, data)
return data.Success
}
func tryCreateAccount(l *ldap.Conn, data *NewAccountData, pass1 string, pass2 string, invitedBy string) {
checkFailed := false
// Check if username is correct
if match, err := regexp.MatchString("^[a-z0-9._-]+$", data.Username); !(err == nil && match) {
data.ErrorInvalidUsername = true
checkFailed = true
}
// Check if user exists
userDn := config.UserNameAttr + "=" + data.Username + "," + config.UserBaseDN
searchRq := ldap.NewSearchRequest(
userDn,
ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
"(objectclass=*)",
[]string{"dn"},
nil)
sr, err := l.Search(searchRq)
if err != nil {
data.ErrorMessage = err.Error()
checkFailed = true
}
if len(sr.Entries) > 0 {
data.ErrorUsernameTaken = true
checkFailed = true
}
// Check that password is long enough
if len(pass1) < 8 {
data.ErrorPasswordTooShort = true
checkFailed = true
}
if pass1 != pass2 {
data.ErrorPasswordMismatch = true
checkFailed = true
}
if checkFailed {
return
}
// Actually create user
req := ldap.NewAddRequest(userDn, nil)
req.Attribute("objectclass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"})
req.Attribute("structuralobjectclass", []string{"inetOrgPerson"})
pw, err := SSHAEncode(pass1)
if err != nil {
data.ErrorMessage = err.Error()
return
}
req.Attribute("userpassword", []string{pw})
req.Attribute("invitedby", []string{invitedBy})
if len(data.DisplayName) > 0 {
req.Attribute("displayname", []string{data.DisplayName})
}
if len(data.GivenName) > 0 {
req.Attribute("givenname", []string{data.GivenName})
}
if len(data.Surname) > 0 {
req.Attribute("sn", []string{data.Surname})
}
if len(config.InvitedMailFormat) > 0 {
email := strings.ReplaceAll(config.InvitedMailFormat, "{}", data.Username)
req.Attribute("mail", []string{email})
}
err = l.Add(req)
if err != nil {
data.ErrorMessage = err.Error()
return
}
for _, group := range config.InvitedAutoGroups {
req := ldap.NewModifyRequest(group, nil)
req.Add("member", []string{userDn})
err = l.Modify(req)
if err != nil {
data.WarningMessage += fmt.Sprintf("Cannot add to %s: %s\n", group, err.Error())
}
}
data.Success = true
}
// ---- Code generation ----
type SendCodeData struct {
ErrorMessage string
ErrorInvalidEmail bool
Success bool
CodeDisplay string
CodeSentTo string
WebBaseAddress string
CanAdmin bool
}
type CodeMailFields struct {
From string
To string
Code string
InviteFrom string
WebBaseAddress string
CanAdmin bool
}
func handleInviteSendCode(w http.ResponseWriter, r *http.Request) {
templateInviteSendCode := getTemplate("invite_send_code.html")
login := checkInviterLogin(w, r)
if login == nil {
return
}
// carLicense
if r.Method == "POST" {
r.ParseForm()
data := &SendCodeData{
WebBaseAddress: config.WebAddress,
}
// modify_request := ldap.NewModifyRequest(login.UserEntry.DN, nil)
// // choice := strings.Join(r.Form["choice"], "")
// // sendto := strings.Join(r.Form["sendto"], "")
code, code_id, code_pw := genCode()
log.Printf(fmt.Sprintf("272: %v %v %v", code, code_id, code_pw))
// // Create invitation object in database
// modify_request.Add("carLicense", []string{fmt.Sprintf("%s,%s,%s",code, code_id, code_pw)})
// err := login.conn.Modify(modify_request)
// if err != nil {
// data.ErrorMessage = err.Error()
// // return
// } else {
// data.Success = true
// data.CodeDisplay = code
// }
log.Printf(fmt.Sprintf("279: %v %v %v", code, code_id, code_pw))
addReq := ldap.NewAddRequest("documentIdentifier="+code_id+","+config.InvitationBaseDN, nil)
addReq.Attribute("objectClass", []string{"top", "document", "simpleSecurityObject"})
addReq.Attribute("cn", []string{code})
addReq.Attribute("userPassword", []string{code_pw})
addReq.Attribute("documentIdentifier", []string{code_id})
log.Printf(fmt.Sprintf("285: %v %v %v", code, code_id, code_pw))
log.Printf(fmt.Sprintf("286: %v", addReq))
err := login.conn.Add(addReq)
if err != nil {
data.ErrorMessage = err.Error()
// return
} else {
data.Success = true
data.CodeDisplay = code
}
data.CanAdmin = login.CanAdmin
templateInviteSendCode.Execute(w, data)
// if choice == "display" || choice == "send" {
// log.Printf(fmt.Sprintf("260: %v %v %v %v", login, choice, sendto, data))
// trySendCode(login, choice, sendto, data)
// }
}
}
func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCodeData) {
log.Printf(fmt.Sprintf("269: %v %v %v %v", login, choice, sendto, data))
// Generate code
code, code_id, code_pw := genCode()
log.Printf(fmt.Sprintf("272: %v %v %v", code, code_id, code_pw))
// Create invitation object in database
// len_base_dn := len(strings.Split(config.BaseDN, ","))
// dn_split := strings.Split(super_dn, ",")
// for i := len_base_dn + 1; i <= len(dn_split); i++ {
// path = append(path, PathItem{
// DN: strings.Join(dn_split[len(dn_split)-i:len(dn_split)], ","),
// Identifier: dn_split[len(dn_split)-i],
// })
// }
// data := &SendCodeData{
// WebBaseAddress: config.WebAddress,
// }
// // Handle data
// data := &CreateData{
// SuperDN: super_dn,
// Path: path,
// }
// data.IdType = config.UserNameAttr
// data.StructuralObjectClass = "inetOrgPerson"
// data.ObjectClass = "inetOrgPerson\norganizationalPerson\nperson\ntop"
// data.IdValue = strings.TrimSpace(strings.Join(r.Form["idvalue"], ""))
// data.DisplayName = strings.TrimSpace(strings.Join(r.Form["displayname"], ""))
// data.GivenName = strings.TrimSpace(strings.Join(r.Form["givenname"], ""))
// data.Mail = strings.TrimSpace(strings.Join(r.Form["mail"], ""))
// data.Member = strings.TrimSpace(strings.Join(r.Form["member"], ""))
// data.Description = strings.TrimSpace(strings.Join(r.Form["description"], ""))
// data.SN = strings.TrimSpace(strings.Join(r.Form["sn"], ""))
// object_class := []string{}
// for _, oc := range strings.Split(data.ObjectClass, "\n") {
// x := strings.TrimSpace(oc)
// if x != "" {
// object_class = append(object_class, x)
// }
// }
// dn := data.IdType + "=" + data.IdValue + "," + super_dn
// req := ldap.NewAddRequest(dn, nil)
// req.Attribute("objectclass", object_class)
// // req.Attribute("mail", []string{data.IdValue})
// /*
// if data.StructuralObjectClass != "" {
// req.Attribute("structuralobjectclass", []string{data.StructuralObjectClass})
// }
// */
// if data.DisplayName != "" {
// req.Attribute("displayname", []string{data.DisplayName})
// }
// if data.GivenName != "" {
// req.Attribute("givenname", []string{data.GivenName})
// }
// if data.Mail != "" {
// req.Attribute("mail", []string{data.Mail})
// }
// if data.Member != "" {
// req.Attribute("member", []string{data.Member})
// }
// if data.SN != "" {
// req.Attribute("sn", []string{data.SN})
// }
// if data.Description != "" {
// req.Attribute("description", []string{data.Description})
// }
// err := login.conn.Add(req)
// // log.Printf(fmt.Sprintf("899: %v",err))
// // log.Printf(fmt.Sprintf("899: %v",req))
// // log.Printf(fmt.Sprintf("899: %v",data))
// if err != nil {
// data.Error = err.Error()
// } else {
// if template == "ml" {
// http.Redirect(w, r, "/admin/mailing/"+data.IdValue, http.StatusFound)
// } else {
// http.Redirect(w, r, "/admin/ldap/"+dn, http.StatusFound)
// }
// }
// inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN
// req := ldap.NewAddRequest(inviteDn, nil)
// pw, err := SSHAEncode(code_pw)
// if err != nil {
// data.ErrorMessage = err.Error()
// return
// }
// req.Attribute("employeeNumber", []string{pw})
// req.Attribute("objectclass", []string{"top", "invitationCode"})
// err = login.conn.Add(req)
// if err != nil {
// log.Printf(fmt.Sprintf("286: %v", req))
// 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(templatePath + "/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)
log.Printf(fmt.Sprintf("342: %v %v %v", code, code_id, code_pw))
return code, code_id, code_pw
}
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 := argon2.IDKey([]byte(code_digits), []byte("Guichet ID"), 2, 64*1024, 4, 32)
pw_hash := argon2.IDKey([]byte(code_digits), []byte("Guichet PW"), 2, 64*1024, 4, 32)
code_id = hex.EncodeToString(id_hash[:8])
code_pw = hex.EncodeToString(pw_hash[:16])
return code_id, code_pw
}