diff --git a/controllers/controller.go b/controllers/controller.go new file mode 100644 index 0000000..f223473 --- /dev/null +++ b/controllers/controller.go @@ -0,0 +1,73 @@ +/* +Routes the requests to the app +*/ +package controllers + +import ( + "guichet/models" + "guichet/views" + "net/http" + + "github.com/gorilla/mux" +) + + +var staticPath = "./static" + + + +/* +Create the different routes +*/ +func MakeGVRouter() (*mux.Router, error) { + r := mux.NewRouter() + r.HandleFunc("/", views.HandleHome) + + r.HandleFunc("/session/logout", views.HandleLogout) + + r.HandleFunc("/user", views.HandleUser) + r.HandleFunc("/user/new", views.HandleInviteNewAccount) + r.HandleFunc("/user/new/", views.HandleInviteNewAccount) + r.HandleFunc("/user/wait", views.HandleUserWait) + r.HandleFunc("/user/mail", views.HandleUserMail) + + r.HandleFunc("/picture/{name}", views.HandleDownloadPicture) + + r.HandleFunc("/passwd", views.HandlePasswd) + r.HandleFunc("/passwd/lost", views.HandleLostPassword) + r.HandleFunc("/passwd/lost/{code}", views.HandleFoundPassword) + + r.HandleFunc("/admin", views.HandleHome) + r.HandleFunc("/admin/activate", views.HandleAdminActivateUsers) + r.HandleFunc("/admin/unactivate/{cn}", views.HandleAdminUnactivateUser) + r.HandleFunc("/admin/activate/{cn}", views.HandleAdminActivateUser) + r.HandleFunc("/admin/users", views.HandleAdminUsers) + r.HandleFunc("/admin/groups", views.HandleAdminGroups) + r.HandleFunc("/admin/ldap/{dn}", views.HandleAdminLDAP) + r.HandleFunc("/admin/create/{template}/{super_dn}", views.HandleAdminCreate) + + // r.HandleFunc("/directory/search", views.HandleDirectorySearch) + // r.HandleFunc("/directory", views.HandleDirectory) + // r.HandleFunc("/garage/key", views.HandleGarageKey) + // r.HandleFunc("/garage/website", views.HandleGarageWebsiteList) + // r.HandleFunc("/garage/website/new", views.HandleGarageWebsiteNew) + // r.HandleFunc("/garage/website/b/{bucket}", views.HandleGarageWebsiteInspect) + + // r.HandleFunc("/user/send_code", views.HandleInviteSendCode) + + // r.HandleFunc("/invitation/{code}", views.HandleInvitationCode) + + // r.HandleFunc("/admin-mailing", views.HandleAdminMailing) + // r.HandleFunc("/admin/mailing/{id}", views.HandleAdminMailingList) + + staticFiles := http.FileServer(http.Dir(staticPath)) + r.Handle("/static/{file:.*}", http.StripPrefix("/static/", staticFiles)) + config_file := models.ReadConfig() + + // log.Printf("Starting HTTP server on %s", config.HttpBindAddr) + err := http.ListenAndServe(config_file.HttpBindAddr, views.LogRequest(r)) + + return r, err +} + + diff --git a/garage.notgo b/garage.notgo new file mode 100644 index 0000000..8567200 --- /dev/null +++ b/garage.notgo @@ -0,0 +1,239 @@ +package models + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "strings" + + garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" + "github.com/go-ldap/ldap/v3" + "github.com/gorilla/mux" +) + +var config = ReadConfig() + +func gadmin() (*garage.APIClient, context.Context) { + // Set Host and other parameters + configuration := garage.NewConfiguration() + configuration.Host = config.S3AdminEndpoint + + // We can now generate a client + client := garage.NewAPIClient(configuration) + + // Authentication is handled through the context pattern + ctx := context.WithValue(context.Background(), garage.ContextAccessToken, config.S3AdminToken) + return client, ctx +} + +func grgCreateKey(name string) (*garage.KeyInfo, error) { + client, ctx := gadmin() + + kr := garage.AddKeyRequest{Name: &name} + resp, _, err := client.KeyApi.AddKey(ctx).AddKeyRequest(kr).Execute() + if err != nil { + fmt.Printf("%+v\n", err) + return nil, err + } + return resp, nil +} + +func grgGetKey(accessKey string) (*garage.KeyInfo, error) { + client, ctx := gadmin() + + resp, _, err := client.KeyApi.GetKey(ctx, accessKey).Execute() + if err != nil { + fmt.Printf("%+v\n", err) + return nil, err + } + return resp, nil +} + +func grgCreateWebsite(gkey, bucket string) (*garage.BucketInfo, error) { + client, ctx := gadmin() + + br := garage.NewCreateBucketRequest() + br.SetGlobalAlias(bucket) + + // Create Bucket + binfo, _, err := client.BucketApi.CreateBucket(ctx).CreateBucketRequest(*br).Execute() + if err != nil { + fmt.Printf("%+v\n", err) + return nil, err + } + + // Allow user's key + ar := garage.AllowBucketKeyRequest{ + BucketId: *binfo.Id, + AccessKeyId: gkey, + Permissions: *garage.NewAllowBucketKeyRequestPermissions(true, true, true), + } + binfo, _, err = client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute() + if err != nil { + fmt.Printf("%+v\n", err) + return nil, err + } + + // Expose website and set quota + wr := garage.NewUpdateBucketRequestWebsiteAccess() + wr.SetEnabled(true) + wr.SetIndexDocument("index.html") + wr.SetErrorDocument("error.html") + + qr := garage.NewUpdateBucketRequestQuotas() + qr.SetMaxSize(1024 * 1024 * 50) // 50MB + qr.SetMaxObjects(10000) //10k objects + + ur := garage.NewUpdateBucketRequest() + ur.SetWebsiteAccess(*wr) + ur.SetQuotas(*qr) + + binfo, _, err = client.BucketApi.UpdateBucket(ctx, *binfo.Id).UpdateBucketRequest(*ur).Execute() + if err != nil { + fmt.Printf("%+v\n", err) + return nil, err + } + + // Return updated binfo + return binfo, nil +} + +func grgGetBucket(bid string) (*garage.BucketInfo, error) { + client, ctx := gadmin() + + resp, _, err := client.BucketApi.GetBucketInfo(ctx, bid).Execute() + if err != nil { + fmt.Printf("%+v\n", err) + return nil, err + } + return resp, nil + +} + +func checkLoginAndS3(w http.ResponseWriter, r *http.Request) (*LoginStatus, *garage.KeyInfo, error) { + login := checkLogin(w, r) + if login == nil { + return nil, nil, errors.New("LDAP login failed") + } + + keyID := login.UserEntry.GetAttributeValue("garage_s3_access_key") + if keyID == "" { + keyPair, err := grgCreateKey(login.Info.Username) + if err != nil { + return login, nil, err + } + modify_request := ldap.NewModifyRequest(login.Info.DN, nil) + modify_request.Replace("garage_s3_access_key", []string{*keyPair.AccessKeyId}) + // @FIXME compatibility feature for bagage (SFTP+webdav) + // you can remove it once bagage will be updated to fetch the key from garage directly + // or when bottin will be able to dynamically fetch it. + modify_request.Replace("garage_s3_secret_key", []string{*keyPair.SecretAccessKey}) + err = login.conn.Modify(modify_request) + return login, keyPair, err + } + // Note: we could simply return the login info, but LX asked we do not + // store the secrets in LDAP in the future. + keyPair, err := grgGetKey(keyID) + return login, keyPair, err +} + + + +func HandleGarageKey(w http.ResponseWriter, r *http.Request) { + login, s3key, err := checkLoginAndS3(w, r) + if err != nil { + log.Println(err) + return + } + view := keyView{Status: login, Key: s3key} + + tKey := getTemplate("garage/key.html") + tKey.Execute(w, &view) +} + + + +func HandleGarageWebsiteList(w http.ResponseWriter, r *http.Request) { + login, s3key, err := checkLoginAndS3(w, r) + if err != nil { + log.Println(err) + return + } + view := webListView{Status: login, Key: s3key} + + tWebsiteList := getTemplate("garage/website/list.html") + tWebsiteList.Execute(w, &view) +} + +func HandleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) { + _, s3key, err := checkLoginAndS3(w, r) + if err != nil { + log.Println(err) + return + } + + tWebsiteNew := getTemplate("garage/website/new.html") + if r.Method == "POST" { + r.ParseForm() + log.Println(r.Form) + + bucket := strings.Join(r.Form["bucket"], "") + if bucket == "" { + bucket = strings.Join(r.Form["bucket2"], "") + } + if bucket == "" { + log.Println("Form empty") + // @FIXME we need to return the error to the user + tWebsiteNew.Execute(w, nil) + return + } + + binfo, err := grgCreateWebsite(*s3key.AccessKeyId, bucket) + if err != nil { + log.Println(err) + // @FIXME we need to return the error to the user + tWebsiteNew.Execute(w, nil) + return + } + + http.Redirect(w, r, "/garage/website/b/"+*binfo.Id, http.StatusFound) + return + } + + tWebsiteNew.Execute(w, nil) +} + + + +func HandleGarageWebsiteInspect(w http.ResponseWriter, r *http.Request) { + login, s3key, err := checkLoginAndS3(w, r) + if err != nil { + log.Println(err) + return + } + + bucketId := mux.Vars(r)["bucket"] + binfo, err := grgGetBucket(bucketId) + if err != nil { + log.Println(err) + return + } + + wc := binfo.GetWebsiteConfig() + q := binfo.GetQuotas() + + view := webInspectView{ + Status: login, + Key: s3key, + Bucket: binfo, + IndexDoc: (&wc).GetIndexDocument(), + ErrorDoc: (&wc).GetErrorDocument(), + MaxObjects: (&q).GetMaxObjects(), + MaxSize: (&q).GetMaxSize(), + } + + tWebsiteInspect := getTemplate("garage/website/inspect.html") + tWebsiteInspect.Execute(w, &view) +} diff --git a/models/config.go b/models/config.go new file mode 100644 index 0000000..951878c --- /dev/null +++ b/models/config.go @@ -0,0 +1,52 @@ +/* +config Handles reading the config.json file at the root and processing the settings +*/ +package models + +type ConfigFile struct { + HttpBindAddr string `json:"http_bind_addr"` + 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"` + + MailingBaseDN string `json:"mailing_list_base_dn"` + MailingNameAttr string `json:"mailing_list_name_attr"` + MailingGuestsBaseDN string `json:"mailing_list_guest_user_base_dn"` + + 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"` + + S3AdminEndpoint string `json:"s3_admin_endpoint"` + S3AdminToken string `json:"s3_admin_token"` + + S3Endpoint string `json:"s3_endpoint"` + S3AccessKey string `json:"s3_access_key"` + S3SecretKey string `json:"s3_secret_key"` + S3Region string `json:"s3_region"` + S3Bucket string `json:"s3_bucket"` + + Org string `json:"org"` + DomainName string `json:"domain_name"` + NewUserDN string `json:"new_user_dn"` + NewUserPassword string `json:"new_user_password"` + NewUsersBaseDN string `json:"new_users_base_dn"` + NewUserDefaultDomain string `json:"new_user_default_domain"` +} + diff --git a/models/ldap.go b/models/ldap.go new file mode 100644 index 0000000..f0f5e2e --- /dev/null +++ b/models/ldap.go @@ -0,0 +1,118 @@ +/* +Utilities related to LDAP +*/ +package models + +import ( + "log" + "sort" + "strings" + + "github.com/go-ldap/ldap/v3" +) + +var config = ReadConfig() +const FIELD_NAME_PROFILE_PICTURE = "profilePicture" +const FIELD_NAME_DIRECTORY_VISIBILITY = "directoryVisibility" + +type SearchResult struct { + DN string + Id string + DisplayName string + Email string + Description string + ProfilePicture string +} +type SearchResults struct { + Results []SearchResult +} + + +func (r *SearchResults) Len() int { + return len(r.Results) +} + +func (r *SearchResults) Less(i, j int) bool { + return r.Results[i].Id < r.Results[j].Id +} + +func (r *SearchResults) Swap(i, j int) { + r.Results[i], r.Results[j] = r.Results[j], r.Results[i] +} + + +func ContainsI(a string, b string) bool { + return strings.Contains( + strings.ToLower(a), + strings.ToLower(b), + ) +} + + +// New account creation directly from interface + + +func OpenNewUserLdap(config *ConfigFile) (*ldap.Conn, error) { + l, err := openLdap(config) + if err != nil { + log.Printf("OpenNewUserLdap 1 : %v %v", err, l) + log.Printf("OpenNewUserLdap 1 : %v", config) + // data.Common.ErrorMessage = err.Error() + } + err = l.Bind(config.NewUserDN, config.NewUserPassword) + if err != nil { + log.Printf("OpenNewUserLdap Bind : %v", err) + log.Printf("OpenNewUserLdap Bind : %v", config.NewUserDN) + log.Printf("OpenNewUserLdap Bind : %v", config.NewUserPassword) + log.Printf("OpenNewUserLdap Bind : %v", config) + // data.Common.ErrorMessage = err.Error() + } + return l, err +} + +func DoDirectorySearch(ldapConn *ldap.Conn, input string) (SearchResults, error) { + //Search values with ldap and filter + + searchRequest := ldap.NewSearchRequest( + config.UserBaseDN, + ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, + "(&(objectclass=organizationalPerson)("+FIELD_NAME_DIRECTORY_VISIBILITY+"=on))", + []string{ + config.UserNameAttr, + "displayname", + "mail", + "description", + FIELD_NAME_PROFILE_PICTURE, + }, + nil) + //Transform the researh's result in a correct struct to send JSON + results := []SearchResult{} + sr, err := ldapConn.Search(searchRequest) + if err != nil { + return SearchResults{}, err + } + + + + for _, values := range sr.Entries { + if input == "" || + ContainsI(values.GetAttributeValue(config.UserNameAttr), input) || + ContainsI(values.GetAttributeValue("displayname"), input) || + ContainsI(values.GetAttributeValue("mail"), input) { + results = append(results, SearchResult{ + DN: values.DN, + Id: values.GetAttributeValue(config.UserNameAttr), + DisplayName: values.GetAttributeValue("displayname"), + Email: values.GetAttributeValue("mail"), + Description: values.GetAttributeValue("description"), + ProfilePicture: values.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE), + }) + } + } + // sort.Sort(&results) + search_results := SearchResults{ + Results: results, + } + sort.Sort(&search_results) + return search_results, nil +} \ No newline at end of file diff --git a/models/model.go b/models/model.go new file mode 100644 index 0000000..fb62639 --- /dev/null +++ b/models/model.go @@ -0,0 +1,67 @@ +/* +Centralises the models used in this application +*/ + +package models + +import ( + // "crypto/tls" + // "log" + // "net" + + "github.com/go-ldap/ldap/v3" +) + + +/* +Represents a user +*/ +type User struct { + DN string + CN string + GivenName string + DisplayName string + Mail string + SN string + UID string + Description string + Password string + OtherMailbox string + CanAdmin bool + CanInvite bool + UserEntry *ldap.Entry + SeeAlso string +} + + + + + + +// func openLdap(config *ConfigFile) (*ldap.Conn, error) { +// var ldapConn *ldap.Conn +// var err error +// if config.LdapTLS { +// tlsConf := &tls.Config{ +// ServerName: config.LdapServerAddr, +// InsecureSkipVerify: true, +// } +// ldapConn, err = ldap.DialTLS("tcp", net.JoinHostPort(config.LdapServerAddr, "636"), tlsConf) +// } else { +// ldapConn, err = ldap.DialURL("ldap://" + config.LdapServerAddr) +// } +// if err != nil { +// log.Printf("openLDAP %v", err) +// log.Printf("openLDAP %v", config.LdapServerAddr) +// } +// return ldapConn, err + +// // l, err := ldap.DialURL(config.LdapServerAddr) +// // if err != nil { +// // log.Printf(fmt.Sprint("Erreur connect LDAP %v", err)) +// // log.Printf(fmt.Sprint("Erreur connect LDAP %v", config.LdapServerAddr)) +// // return nil +// // } else { +// // return l +// // } +// } diff --git a/models/modelutils.go b/models/modelutils.go new file mode 100644 index 0000000..1f08fa1 --- /dev/null +++ b/models/modelutils.go @@ -0,0 +1,135 @@ +package models + +import ( + "bytes" + "crypto/tls" + "log" + "net" + "net/smtp" + + "html/template" + + "github.com/go-ldap/ldap/v3" + // "golang.org/x/text/encoding/unicode" + "encoding/json" + + "io/ioutil" + + "os" + + "flag" +) + +// + +func ReadConfig() ConfigFile { + // Default configuration values for certain fields + flag.Parse() + var configFlag = flag.String("config", "./config.json", "Configuration file path") + + config_file := ConfigFile{ + HttpBindAddr: ":9991", + LdapServerAddr: "ldap://127.0.0.1:389", + + UserNameAttr: "uid", + GroupNameAttr: "gid", + + InvitationNameAttr: "cn", + InvitedAutoGroups: []string{}, + + Org: "ResDigita", + } + + _, err := os.Stat(*configFlag) + if os.IsNotExist(err) { + log.Fatalf("Could not find Guichet configuration file at %s. Please create this file, for exemple starting with config.json.exemple and customizing it for your deployment.", *configFlag) + } + + if err != nil { + log.Fatal(err) + } + + bytes, err := ioutil.ReadFile(*configFlag) + if err != nil { + log.Fatal(err) + } + + err = json.Unmarshal(bytes, &config_file) + if err != nil { + log.Fatal(err) + } + + return config_file +} + + + +type EmailContentVarsTplData struct { + Code string + SendAddress string + InviteFrom string +} + +// Data to be passed to an email for sending +type SendMailTplData struct { + // Sender of the email + To string + // Receiver of the email + From string + // Relative path (without leading /) to the email template in the templates folder + // usually ending in .txt + RelTemplatePath string + // Variables to be included in the template of the email + EmailContentVars EmailContentVarsTplData +} + + + +func openLdap(config *ConfigFile) (*ldap.Conn, error) { + var ldapConn *ldap.Conn + var err error + if config.LdapTLS { + tlsConf := &tls.Config{ + ServerName: config.LdapServerAddr, + InsecureSkipVerify: true, + } + ldapConn, err = ldap.DialTLS("tcp", net.JoinHostPort(config.LdapServerAddr, "636"), tlsConf) + } else { + ldapConn, err = ldap.DialURL("ldap://" + config.LdapServerAddr) + } + if err != nil { + log.Printf("openLDAP %v", err) + log.Printf("openLDAP %v", config.LdapServerAddr) + } + return ldapConn, err + + // l, err := ldap.DialURL(config.LdapServerAddr) + // if err != nil { + // log.Printf(fmt.Sprint("Erreur connect LDAP %v", err)) + // log.Printf(fmt.Sprint("Erreur connect LDAP %v", config.LdapServerAddr)) + // return nil + // } else { + // return l + // } +} + + + +// Sends an email according to the enclosed information +func sendMail(sendMailTplData SendMailTplData) error { + log.Printf("sendMail") + templateMail := template.Must(template.ParseFiles( "./templates" + sendMailTplData.RelTemplatePath)) + buf := bytes.NewBuffer([]byte{}) + err := templateMail.Execute(buf, sendMailTplData) + message := buf.Bytes() + auth := smtp.PlainAuth("", config.SMTPUsername, config.SMTPPassword, config.SMTPServer) + log.Printf("auth: %v", auth) + err = smtp.SendMail(config.SMTPServer+":587", auth, config.SMTPUsername, []string{sendMailTplData.To}, message) + if err != nil { + log.Printf("sendMail smtp.SendMail %v", err) + log.Printf("sendMail smtp.SendMail %v", sendMailTplData) + return err + } + log.Printf("Mail sent.") + return err +} diff --git a/models/passwd.go b/models/passwd.go new file mode 100644 index 0000000..b080f28 --- /dev/null +++ b/models/passwd.go @@ -0,0 +1,222 @@ +/* +gpas is GVoisin password reset +*/ + +package models + +import ( + "bytes" + "errors" + "fmt" + "html/template" + "log" + "math/rand" + + // "github.com/emersion/go-sasl" + // "github.com/emersion/go-smtp" + "net/smtp" + + "github.com/go-ldap/ldap/v3" + + // "strings" + b64 "encoding/base64" +) + +var templatePath = "./templates" + +type CodeMailFields struct { + From string + To string + Code string + InviteFrom string + WebBaseAddress string + Common NestedCommonTplData +} +type NestedCommonTplData struct { + Error string + ErrorMessage string + CanAdmin bool + CanInvite bool + LoggedIn bool + Success bool + WarningMessage string + WebsiteName string + WebsiteURL string +} +// type InvitationAccount struct { +// UID string +// Password string +// BaseDN string +// } + +// Suggesting a 12 char password with some excentrics +func SuggestPassword() string { + password := "" + chars := "abcdfghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*+_-=" + for i := 0; i < 12; i++ { + password += string([]rune(chars)[rand.Intn(len(chars))]) + } + return password +} + +// 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 getInvitationBaseDN(config *ConfigFile) string { + return config.InvitationBaseDN +} + +func PasswordLost(user User, config *ConfigFile, ldapConn *ldap.Conn) error { + if user.CN == "" && user.Mail == "" && user.OtherMailbox == "" { + return errors.New("Il n'y a pas de quoi identifier l'utilisateur") + } + searchFilter := "(|" + if user.CN != "" { + searchFilter += "(cn=" + user.UID + ")" + } + if user.Mail != "" { + searchFilter += "(mail=" + user.Mail + ")" + } + if user.OtherMailbox != "" { + searchFilter += "(carLicense=" + user.OtherMailbox + ")" + } + searchFilter += ")" + searchReq := ldap.NewSearchRequest(config.UserBaseDN, ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, searchFilter, []string{"cn", "uid", "mail", "carLicense", "sn", "displayName", "givenName"}, nil) + searchRes, err := ldapConn.Search(searchReq) + if err != nil { + log.Printf("PasswordLost search : %v %v", err, ldapConn) + log.Printf("PasswordLost search : %v", searchReq) + log.Printf("PasswordLost search : %v", searchRes) + log.Printf("PasswordLost search: %v", user) + return err + } + if len(searchRes.Entries) == 0 { + log.Printf("Il n'y a pas d'utilisateur qui correspond %v", searchReq) + return errors.New("Il n'y a pas d'utilisateur qui correspond") + } + // log.Printf("PasswordLost 58 : %v", user) + // log.Printf("PasswordLost 59 : %v", searchRes.Entries[0]) + // log.Printf("PasswordLost 60 : %v", searchRes.Entries[0].GetAttributeValue("cn")) + // log.Printf("PasswordLost 61 : %v", searchRes.Entries[0].GetAttributeValue("uid")) + // log.Printf("PasswordLost 62 : %v", searchRes.Entries[0].GetAttributeValue("mail")) + // log.Printf("PasswordLost 63 : %v", searchRes.Entries[0].GetAttributeValue("carLicense")) + // Préparation du courriel à envoyer + + delReq := ldap.NewDelRequest("uid="+searchRes.Entries[0].GetAttributeValue("cn")+","+config.InvitationBaseDN, nil) + err = ldapConn.Del(delReq) + + user.Password = SuggestPassword() + user.DN = "uid=" + searchRes.Entries[0].GetAttributeValue("cn") + "," + config.InvitationBaseDN + user.UID = searchRes.Entries[0].GetAttributeValue("cn") + user.CN = searchRes.Entries[0].GetAttributeValue("cn") + user.Mail = searchRes.Entries[0].GetAttributeValue("mail") + user.OtherMailbox = searchRes.Entries[0].GetAttributeValue("carLicense") + code := b64.URLEncoding.EncodeToString([]byte(user.UID + ";" + user.Password)) + /* Check for outstanding invitation */ + searchReq = ldap.NewSearchRequest(config.InvitationBaseDN, ldap.ScopeSingleLevel, + ldap.NeverDerefAliases, 0, 0, false, "(uid="+user.UID+")", []string{"seeAlso"}, nil) + searchRes, err = ldapConn.Search(searchReq) + if err != nil { + log.Printf(fmt.Sprintf("PasswordLost (Check existing invitation) : %v", err)) + log.Printf(fmt.Sprintf("PasswordLost (Check existing invitation) : %v", user)) + return err + } + // if len(searchRes.Entries) == 0 { + /* Add the invitation */ + addReq := ldap.NewAddRequest( + "uid="+user.UID+","+config.InvitationBaseDN, + nil) + addReq.Attribute("objectClass", []string{"top", "account", "simpleSecurityObject"}) + addReq.Attribute("uid", []string{user.UID}) + addReq.Attribute("userPassword", []string{user.Password}) + addReq.Attribute("seeAlso", []string{config.UserNameAttr + "=" + user.UID + "," + config.UserBaseDN}) + // Password invitation may already exist + + // + err = ldapConn.Add(addReq) + if err != nil { + log.Printf("PasswordLost 83 : %v", err) + log.Printf("PasswordLost 84 : %v", user) + + log.Printf("PasswordLost 84 : %v", addReq) + // // log.Printf("PasswordLost 85 : %v", searchRes.Entries[0])) + // // For some reason I get here even if the entry exists already + // return err + } + // } + ldapNewConn, err := OpenNewUserLdap(config) + if err != nil { + log.Printf("PasswordLost OpenNewUserLdap : %v", err) + } + err = PassWD(user, config, ldapNewConn) + if err != nil { + log.Printf("PasswordLost PassWD : %v", err) + log.Printf("PasswordLost PassWD : %v", user) + log.Printf("PasswordLost PassWD : %v", searchRes.Entries[0]) + return err + } + templateMail := template.Must(template.ParseFiles(templatePath + "/passwd/lost_password_email.txt")) + buf := bytes.NewBuffer([]byte{}) + templateMail.Execute(buf, &CodeMailFields{ + To: user.OtherMailbox, + From: config.MailFrom, + InviteFrom: user.UID, + Code: code, + WebBaseAddress: config.WebAddress, + }) + // message := []byte("Hi " + user.OtherMailbox) + log.Printf("Sending mail to: %s", user.OtherMailbox) + // var auth sasl.Client = nil + // if config.SMTPUsername != "" { + // auth = sasl.NewPlainClient("", config.SMTPUsername, config.SMTPPassword) + // } + message := buf.Bytes() + auth := smtp.PlainAuth("", config.SMTPUsername, config.SMTPPassword, config.SMTPServer) + log.Printf("auth: %v", auth) + err = smtp.SendMail(config.SMTPServer+":587", auth, config.SMTPUsername, []string{user.OtherMailbox}, message) + if err != nil { + log.Printf("email send error %v", err) + return err + } + log.Printf("Mail sent.") + return err +} + +func PasswordFound(user User, config *ConfigFile, ldapConn *ldap.Conn) (string, error) { + l, err := openLdap(config) + if err != nil { + log.Printf("PasswordFound openLdap %v", err) + // log.Printf("PasswordFound openLdap Config : %v", config) + return "", err + } + if user.DN == "" && user.UID != "" { + user.DN = "uid=" + user.UID + "," + config.InvitationBaseDN + } + err = l.Bind(user.DN, user.Password) + if err != nil { + log.Printf("PasswordFound l.Bind %v", err) + log.Printf("PasswordFound l.Bind %v", user.DN) + log.Printf("PasswordFound l.Bind %v", user.UID) + return "", err + } + searchReq := ldap.NewSearchRequest(user.DN, ldap.ScopeBaseObject, + ldap.NeverDerefAliases, 0, 0, false, "(uid="+user.UID+")", []string{"seeAlso"}, nil) + var searchRes *ldap.SearchResult + searchRes, err = ldapConn.Search(searchReq) + if err != nil { + log.Printf("PasswordFound %v", err) + log.Printf("PasswordFound %v", searchReq) + log.Printf("PasswordFound %v", ldapConn) + log.Printf("PasswordFound %v", searchRes) + return "", err + } + if len(searchRes.Entries) == 0 { + log.Printf("PasswordFound %v", err) + log.Printf("PasswordFound %v", searchReq) + log.Printf("PasswordFound %v", ldapConn) + log.Printf("PasswordFound %v", searchRes) + return "", err + } + delReq := ldap.NewDelRequest("uid="+user.CN+","+config.InvitationBaseDN, nil) + ldapConn.Del(delReq) + return searchRes.Entries[0].GetAttributeValue("seeAlso"), err +} diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..ed3e301 --- /dev/null +++ b/models/user.go @@ -0,0 +1,199 @@ +/* +Model-User Handles everything having to do with the user. +*/ +package models + +import ( + "fmt" + "log" + "strings" + + "github.com/go-ldap/ldap/v3" +) + +func replaceIfContent(modifReq *ldap.ModifyRequest, key string, value string, previousValue string) error { + if value != "" { + modifReq.Replace(key, []string{value}) + } else if previousValue != "" { + modifReq.Delete(key, []string{previousValue}) + } + return nil +} + +func GetUser(user User, config *ConfigFile, ldapConn *ldap.Conn) (*User, error) { + return get(user, config, ldapConn) +} + +func get(user User, config *ConfigFile, ldapConn *ldap.Conn) (*User, error) { + searchReq := ldap.NewSearchRequest( + user.DN, + ldap.ScopeBaseObject, + ldap.NeverDerefAliases, + 0, + 0, + false, + "(objectClass=*)", + []string{ + "cn", + "givenName", + "displayName", + "uid", + "sn", + "mail", + "description", + "carLicense", + }, + nil) + searchRes, err := ldapConn.Search(searchReq) + if err != nil { + log.Printf("get User : %v", err) + log.Printf("get User : %v", searchReq) + log.Printf("get User : %v", searchRes) + return nil, err + } + userEntry := searchRes.Entries[0] + resUser := User{ + DN: user.DN, + GivenName: searchRes.Entries[0].GetAttributeValue("givenName"), + DisplayName: searchRes.Entries[0].GetAttributeValue("displayName"), + Description: searchRes.Entries[0].GetAttributeValue("description"), + SN: searchRes.Entries[0].GetAttributeValue("sn"), + UID: searchRes.Entries[0].GetAttributeValue("uid"), + CN: searchRes.Entries[0].GetAttributeValue("cn"), + Mail: searchRes.Entries[0].GetAttributeValue("mail"), + OtherMailbox: searchRes.Entries[0].GetAttributeValue("carLicense"), + CanAdmin: strings.EqualFold(user.DN, config.AdminAccount), + CanInvite: true, + UserEntry: userEntry, + } + searchReq.BaseDN = config.GroupCanAdmin + searchReq.Filter = "(member=" + user.DN + ")" + searchRes, err = ldapConn.Search(searchReq) + if err == nil { + if len(searchRes.Entries) > 0 { + resUser.CanAdmin = true + } + } + return &resUser, nil +} + +func AddUser(user User, config *ConfigFile, ldapConn *ldap.Conn) error { + return add(user, config, ldapConn) +} + +// Adds a new user +func add(user User, config *ConfigFile, ldapConn *ldap.Conn) error { + log.Printf(fmt.Sprint("Adding New User")) + // LDAP Add Object + dn := user.DN + req := ldap.NewAddRequest(dn, nil) + req.Attribute("objectClass", []string{"top", "person", "organizationalPerson", "inetOrgPerson"}) + if user.DisplayName != "" { + req.Attribute("displayName", []string{user.DisplayName}) + } + if user.GivenName != "" { + req.Attribute("givenName", []string{user.GivenName}) + } + if user.Mail != "" { + req.Attribute("mail", []string{user.Mail}) + } + if user.UID != "" { + req.Attribute("uid", []string{user.UID}) + } + // if user.Member != "" { + // req.Attribute("member", []string{user.Member}) + // } + if user.SN != "" { + req.Attribute("sn", []string{user.SN}) + } + if user.OtherMailbox != "" { + req.Attribute("carLicense", []string{user.OtherMailbox}) + } + if user.Description != "" { + req.Attribute("description", []string{user.Description}) + } + // Add the User + // err := ldapConn.Add(req) + // var ldapNewConn *ldap.Conn + ldapNewConn, err := OpenNewUserLdap(config) + err = ldapNewConn.Add(req) + if err != nil { + log.Printf(fmt.Sprintf("add(User) ldapconn.Add: %v", err)) + log.Printf(fmt.Sprintf("add(User) ldapconn.Add: %v", req)) + log.Printf(fmt.Sprintf("add(User) ldapconn.Add: %v", user)) + //return err + } + // passwordModifyRequest := ldap.NewPasswordModifyRequest(user.DN, "", user.Password) + // _, err = ldapConn.PasswordModify(passwordModifyRequest) + // if err != nil { + // return err + // } + + // Send the email + + newUserLdapConn, _ := OpenNewUserLdap(config) + user.OtherMailbox = "" + err = PasswordLost(user, config, newUserLdapConn) + if err != nil { + log.Printf("add User PasswordLost %v", err) + log.Printf("add User PasswordLost %v", newUserLdapConn) + } + + // sendMailTplData := SendMailTplData{ + // From: "alice@resdigita.org", + // To: user.OtherMailbox, + // RelTemplatePath: "user/new.email.txt", + // EmailContentVars: EmailContentVarsTplData{ + // InviteFrom: "alice@resdigita.org", + // SendAddress: "https://www.gvoisins.org", + // Code: "...", + // }, + // } + // err = sendMail(sendMailTplData) + // if err != nil { + // log.Printf("add(user) sendMail: %v", err) + // log.Printf("add(user) sendMail: %v", user) + // log.Printf("add(user) sendMail: %v", sendMailTplData) + // } + return err +} + +func ModifyUser(user User, config *ConfigFile, ldapConn *ldap.Conn) error { + return modify(user, config, ldapConn) +} + +func modify(user User, config *ConfigFile, ldapConn *ldap.Conn) error { + modify_request := ldap.NewModifyRequest(user.DN, nil) + previousUser, err := get(user, config, ldapConn) + if err != nil { + return err + } + replaceIfContent(modify_request, "displayName", user.DisplayName, previousUser.DisplayName) + replaceIfContent(modify_request, "givenName", user.GivenName, previousUser.GivenName) + replaceIfContent(modify_request, "sn", user.SN, previousUser.SN) + replaceIfContent(modify_request, "carLicense", user.OtherMailbox, user.OtherMailbox) + replaceIfContent(modify_request, "description", user.Description, previousUser.Description) + err = ldapConn.Modify(modify_request) + if err != nil { + log.Printf(fmt.Sprintf("71: %v", err)) + log.Printf(fmt.Sprintf("72: %v", modify_request)) + log.Printf(fmt.Sprintf("73: %v", user)) + return err + } + return nil +} + +func PassWD(user User, config *ConfigFile, ldapConn *ldap.Conn) error { + passwordModifyRequest := ldap.NewPasswordModifyRequest(user.DN, "", user.Password) + _, err := ldapConn.PasswordModify(passwordModifyRequest) + if err != nil { + log.Printf(fmt.Sprintf("model-user PassWD : %v %v", err, ldapConn)) + log.Printf(fmt.Sprintf("model-user PassWD : %v", user)) + } + return err +} + +func Bind(user User, config *ConfigFile, ldapConn *ldap.Conn) error { + return ldapConn.Bind(user.DN, user.Password) +} + diff --git a/views/admin.go b/views/admin.go new file mode 100644 index 0000000..b1b857c --- /dev/null +++ b/views/admin.go @@ -0,0 +1,1029 @@ +package views + +import ( + "fmt" + "guichet/models" + "net/http" + "regexp" + "sort" + "strings" + + "github.com/go-ldap/ldap/v3" + "github.com/gorilla/mux" +) + + +func checkAdminLogin(w http.ResponseWriter, r *http.Request) *LoginStatus { + login := checkLogin(w, r) + if login == nil { + return nil + } + + if !login.Common.CanAdmin { + http.Error(w, "Not authorized to perform administrative operations.", http.StatusUnauthorized) + return nil + } + return login +} + +func (d EntryList) Len() int { + return len(d) +} + +func (d EntryList) Swap(i, j int) { + d[i], d[j] = d[j], d[i] +} + +func (d EntryList) Less(i, j int) bool { + return d[i].DN < d[j].DN +} + +func HandleAdminActivateUsers(w http.ResponseWriter, r *http.Request) { + templateAdminActivateUsers := getTemplate("admin/activate.html") + login := checkAdminLogin(w, r) + if login == nil { + return + } + + searchRequest := ldap.NewSearchRequest( + config.InvitationBaseDN, + ldap.ScopeSingleLevel, + ldap.NeverDerefAliases, + 0, + 0, + false, + fmt.Sprintf("(&(objectClass=organizationalPerson))"), + []string{"cn", "displayName", "givenName", "sn", "mail", "uid"}, + nil) + + sr, err := login.conn.Search(searchRequest) + + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := &AdminUsersTplData{ + Login: NestedLoginTplData{ + Login: login, + }, + UserNameAttr: config.UserNameAttr, + UserBaseDN: config.UserBaseDN, + Users: EntryList(sr.Entries), + Common: NestedCommonTplData{ + CanAdmin: true, + LoggedIn: true, + }, + } + templateAdminActivateUsers.Execute(w, data) + +} + +func HandleAdminActivateUser(w http.ResponseWriter, r *http.Request) { + cn := mux.Vars(r)["cn"] + login := checkAdminLogin(w, r) + if login == nil { + return + } + modifyRequest := *ldap.NewModifyDNRequest("cn="+cn+","+config.InvitationBaseDN, "cn="+cn, true, config.UserBaseDN) + err := login.conn.ModifyDN(&modifyRequest) + if err != nil { + return + } + http.Redirect(w, r, "/admin/activate", http.StatusFound) +} + +func HandleAdminUnactivateUser(w http.ResponseWriter, r *http.Request) { + cn := mux.Vars(r)["cn"] + login := checkAdminLogin(w, r) + if login == nil { + return + } + modifyRequest := *ldap.NewModifyDNRequest("cn="+cn+","+config.UserBaseDN, "cn="+cn, true, config.InvitationBaseDN) + err := login.conn.ModifyDN(&modifyRequest) + if err != nil { + return + } + http.Redirect(w, r, "/admin/users", http.StatusFound) +} + +func HandleAdminUsers(w http.ResponseWriter, r *http.Request) { + templateAdminUsers := getTemplate("admin/users.html") + + login := checkAdminLogin(w, r) + if login == nil { + return + } + + searchRequest := ldap.NewSearchRequest( + config.UserBaseDN, + ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=organizationalPerson))"), + []string{config.UserNameAttr, "dn", "displayName", "givenName", "sn", "mail", "uid", "cn"}, + nil) + + sr, err := login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := &AdminUsersTplData{ + Login: NestedLoginTplData{Login: login}, + UserNameAttr: config.UserNameAttr, + UserBaseDN: config.UserBaseDN, + Users: EntryList(sr.Entries), + Common: NestedCommonTplData{ + CanAdmin: login.Common.CanAdmin, + LoggedIn: false}, + } + sort.Sort(data.Users) + + // addNewUser(NewUser{ + // DN: "cn=newuser@lesgv.com,ou=newusers,dc=resdigita,dc=org", + // CN: "newuser@lesgv.com", + // GivenName: "New", + // SN: "User", + // DisplayName: "New User", + // Mail: "newuser@lesgv.com", + // }, config, login) + + templateAdminUsers.Execute(w, data) +} + +func HandleAdminGroups(w http.ResponseWriter, r *http.Request) { + templateAdminGroups := getTemplate("admin/groups.html") + + login := checkAdminLogin(w, r) + if login == nil { + return + } + + searchRequest := ldap.NewSearchRequest( + config.GroupBaseDN, + ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=groupOfNames))"), + []string{config.GroupNameAttr, "dn", "description"}, + nil) + + sr, err := login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := &AdminGroupsTplData{ + Login: NestedLoginTplData{ + Login: login}, + GroupNameAttr: config.GroupNameAttr, + GroupBaseDN: config.GroupBaseDN, + Groups: EntryList(sr.Entries), + Common: NestedCommonTplData{ + CanAdmin: login.Common.CanAdmin, + LoggedIn: false}, + } + sort.Sort(data.Groups) + + templateAdminGroups.Execute(w, data) +} + +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: NestedLoginTplData{ + Login: login}, + MailingNameAttr: config.MailingNameAttr, + MailingBaseDN: config.MailingBaseDN, + MailingLists: EntryList(sr.Entries), + Common: NestedCommonTplData{ + CanAdmin: login.Common.CanAdmin, + LoggedIn: false}, + } + sort.Sort(data.MailingLists) + + templateAdminMailing.Execute(w, data) +} + +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) + // log.Printf(fmt.Sprintf("198: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else if action == "add-external" { + mail := strings.Join(r.Form["mail"], "") + sn := strings.Join(r.Form["sn"], "") + givenname := strings.Join(r.Form["givenname"], "") + member := strings.Join(r.Form["member"], "") + displayname := strings.Join(r.Form["displayname"], "") + + searchRequest := ldap.NewSearchRequest( + config.UserBaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=organizationalPerson)(mail=%s))", mail), + []string{"dn", "displayname", "mail"}, + nil) + sr, err := login.conn.Search(searchRequest) + if err != nil { + dError = err.Error() + } else { + if len(sr.Entries) == 0 { + if config.MailingGuestsBaseDN != "" { + guestDn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, mail, config.MailingGuestsBaseDN) + req := ldap.NewAddRequest(guestDn, nil) + //req.Attribute("objectclass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"}) + req.Attribute("objectclass", []string{"inetOrgPerson"}) + req.Attribute("mail", []string{fmt.Sprintf("%s", mail)}) + if givenname != "" { + req.Attribute("givenname", []string{givenname}) + } + if member != "" { + req.Attribute("member", []string{member}) + } + if displayname != "" { + req.Attribute("displayname", []string{displayname}) + } + if sn != "" { + req.Attribute("sn", []string{sn}) + } + // log.Printf(fmt.Sprintf("226: %v",req)) + err := login.conn.Add(req) + if err != nil { + dError = err.Error() + } else { + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Add("member", []string{guestDn}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("249: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } + } else { + dError = "Adding guest users not supported, the user must already have an LDAP account." + } + } else if len(sr.Entries) == 1 { + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Add("member", []string{sr.Entries[0].DN}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("264: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else { + dError = fmt.Sprintf("Multiple users exist with email address %s", mail) + } + } + } 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) + // log.Printf(fmt.Sprintf("280: %v",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", "description"}, + 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: NestedLoginTplData{ + Login: login, + }, + MailingNameAttr: config.MailingNameAttr, + MailingBaseDN: config.MailingBaseDN, + + MailingList: ml, + Members: members, + PossibleNewMembers: possibleNewMembers, + AllowGuest: config.MailingGuestsBaseDN != "", + Common: NestedCommonTplData{ + CanAdmin: true, + Error: dError, + Success: dSuccess, + LoggedIn: true}, + } + sort.Sort(data.Members) + sort.Sort(data.PossibleNewMembers) + + templateAdminMailingList.Execute(w, data) +} + +// =================================================== +// LDAP EXPLORER +// =================================================== + +func HandleAdminLDAP(w http.ResponseWriter, r *http.Request) { + templateAdminLDAP := getTemplate("admin/ldap.html") + + login := checkAdminLogin(w, r) + if login == nil { + return + } + + dn := mux.Vars(r)["dn"] + + dError := "" + dSuccess := false + + // Build path + path := []PathItem{ + PathItem{ + DN: config.BaseDN, + Identifier: config.BaseDN, + Active: dn == config.BaseDN, + }, + } + // log.Printf(fmt.Sprintf("434: %v",path)) + + len_base_dn := len(strings.Split(config.BaseDN, ",")) + dn_split := strings.Split(dn, ",") + dn_last_attr := strings.Split(dn_split[0], "=")[0] + 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], + Active: i == len(dn_split), + }) + } + // log.Printf(fmt.Sprintf("446: %v",path)) + + // Handle modification operation + if r.Method == "POST" { + r.ParseForm() + action := strings.Join(r.Form["action"], "") + if action == "modify" { + attr := strings.Join(r.Form["attr"], "") + values := strings.Split(strings.Join(r.Form["values"], ""), "\n") + values_filtered := []string{} + for _, v := range values { + v2 := strings.TrimSpace(v) + if v2 != "" { + values_filtered = append(values_filtered, v2) + } + } + + if len(values_filtered) == 0 { + dError = "Refusing to delete attribute." + } else { + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Replace(attr, values_filtered) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("468: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } + } else if action == "add" { + attr := strings.Join(r.Form["attr"], "") + values := strings.Split(strings.Join(r.Form["values"], ""), "\n") + values_filtered := []string{} + for _, v := range values { + v2 := strings.TrimSpace(v) + if v2 != "" { + values_filtered = append(values_filtered, v2) + } + } + + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Add(attr, values_filtered) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("490: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else if action == "delete" { + attr := strings.Join(r.Form["attr"], "") + + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Replace(attr, []string{}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("503: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else if action == "delete-from-group" { + group := strings.Join(r.Form["group"], "") + modify_request := ldap.NewModifyRequest(group, nil) + modify_request.Delete("member", []string{dn}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("515: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else if action == "add-to-group" { + group := strings.Join(r.Form["group"], "") + modify_request := ldap.NewModifyRequest(group, nil) + modify_request.Add("member", []string{dn}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("527: %v",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) + // log.Printf(fmt.Sprintf("539: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else if action == "delete-object" { + del_request := ldap.NewDelRequest(dn, nil) + err := login.conn.Del(del_request) + if err != nil { + dError = err.Error() + } else { + http.Redirect(w, r, "/admin/ldap/"+strings.Join(dn_split[1:], ","), http.StatusFound) + return + } + } + } + + // Get object and parse it + searchRequest := ldap.NewSearchRequest( + dn, + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectclass=*)"), + []string{}, + nil) + + sr, err := login.conn.Search(searchRequest) + // log.Printf(fmt.Sprintf("569: %v",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 + } + + object := sr.Entries[0] + + // Read object properties and prepare appropriate form fields + props := make(map[string]*PropValues) + for _, attr := range object.Attributes { + name_lower := strings.ToLower(attr.Name) + if name_lower != dn_last_attr { + if existing, ok := props[name_lower]; ok { + existing.Values = append(existing.Values, attr.Values...) + } else { + editable := true + for _, restricted := range []string{ + "creatorsname", "modifiersname", "createtimestamp", + "modifytimestamp", "entryuuid", + } { + if strings.EqualFold(attr.Name, restricted) { + editable = false + break + } + } + deletable := true + for _, restricted := range []string{"objectclass", "structuralobjectclass"} { + if strings.EqualFold(attr.Name, restricted) { + deletable = false + break + } + } + props[name_lower] = &PropValues{ + Name: attr.Name, + Values: attr.Values, + Editable: editable, + Deletable: deletable, + } + } + } + } + + // Check objectclass to determine object type + objectClass := []string{} + if val, ok := props["objectclass"]; ok { + objectClass = val.Values + } + hasMembers, hasGroups, isOrganization := false, false, false + for _, oc := range objectClass { + if strings.EqualFold(oc, "organizationalPerson") || strings.EqualFold(oc, "person") || strings.EqualFold(oc, "inetOrgPerson") { + hasGroups = true + } + if strings.EqualFold(oc, "groupOfNames") { + hasMembers = true + } + if strings.EqualFold(oc, "organization") { + isOrganization = true + } + } + + // Parse member list and prepare form section + members_dn := []string{} + if mp, ok := props["member"]; ok { + members_dn = mp.Values + delete(props, "member") + } + + members := []EntryName{} + possibleNewMembers := []EntryName{} + if len(members_dn) > 0 || hasMembers { + // Lookup all existing users in the server + // to know the DN -> display name correspondance + searchRequest = ldap.NewSearchRequest( + config.UserBaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectClass=organizationalPerson)"), + []string{"dn", "displayname", "description"}, + nil) + sr, err = login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + userMap := make(map[string]string) + for _, ent := range sr.Entries { + userMap[ent.DN] = ent.GetAttributeValue("displayname") + if userMap[ent.DN] == "" { + userMap[ent.DN] = ent.GetAttributeValue("description") + } + } + + // Select members with their name and remove them from map + for _, memdn := range members_dn { + members = append(members, EntryName{ + DN: memdn, + Name: userMap[memdn], + }) + delete(userMap, memdn) + } + + // Create list of members that can be added + for dn, name := range userMap { + entry := EntryName{ + DN: dn, + Name: name, + } + if entry.Name == "" { + entry.Name = entry.DN + } + possibleNewMembers = append(possibleNewMembers, entry) + } + } + + // // Parse group list and prepare form section + // groups_dn := []string{} + // if gp, ok := props["memberof"]; ok { + // groups_dn = gp.Values + // delete(props, "memberof") + // } + + groups := []EntryName{} + possibleNewGroups := []EntryName{} + searchRequest = ldap.NewSearchRequest( + config.GroupBaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=groupOfNames)(member=%s))", dn), + []string{"dn", "displayName", "cn", "description"}, + nil) + // log.Printf(fmt.Sprintf("708: %v",searchRequest)) + sr, err = login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + // log.Printf(fmt.Sprintf("714: %v",sr.Entries)) + for _, ent := range sr.Entries { + groups = append(groups, EntryName{ + DN: ent.DN, + Name: ent.GetAttributeValue("cn"), + }) + } + searchRequest = ldap.NewSearchRequest( + config.GroupBaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=groupOfNames)(!(member=%s)))", dn), + []string{"dn", "displayName", "cn", "description"}, + nil) + // log.Printf(fmt.Sprintf("724: %v",searchRequest)) + sr, err = login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + // log.Printf(fmt.Sprintf("714: %v",sr.Entries)) + for _, ent := range sr.Entries { + possibleNewGroups = append(possibleNewGroups, EntryName{ + DN: ent.DN, + Name: ent.GetAttributeValue("cn"), + }) + } + + // possibleNewGroup.DN = ent.GetAttributeValue("dn") + // possibleNewGroup.Name = ent.GetAttributeValue("cn") + // // log.Printf(fmt.Sprintf("725: %v %v",dn, ent.GetAttributeValue("member"))) + // for _, member := range ent .GetAttributeValue("member") { + // // // log.Printf(fmt.Sprintf("725: %v %v",dn, member)) + // if ent.GetAttributeValue("member") == dn { + // groups = append(groups,possibleNewGroup,) + // possibleNewGroup.DN = "" + // possibleNewGroup.Name = "" + // } + // // } + // if possibleNewGroup.DN != "" { + // possibleNewGroups = append(possibleNewGroups,possibleNewGroup,) + // possibleNewGroup = EntryName{} + // } + + // groupMap[.DN] = ent.GetAttributeValue("displayName") + // if groupMap[.DN] == "" { + // groupMap[.DN] = ent.GetAttributeValue("cn") + // } + // if groupMap[.DN] == "" { + // groupMap[.DN] = ent.GetAttributeValue("description") + // } + // } + + // // Calculate list of current groups + // // log.Printf(fmt.Sprintf("%v",groups_dn)) + // for _, grpdn := range groups_dn { + // // log.Printf(fmt.Sprintf("%v",grpdn)) + // groups = append(groups, EntryName{ + // DN: grpdn, + // Name: groupMap[grpdn], + // }) + // delete(groupMap, grpdn) + // } + + // // Calculate list of possible new groups + // for dn, name := range groupMap { + // entry := EntryName{ + // DN: dn, + // Name: name, + // } + // if entry.Name == "" { + // entry.Name = entry.DN + // } + // possibleNewGroups = append(possibleNewGroups, entry) + // } + // } + + // Get children + searchRequest = ldap.NewSearchRequest( + dn, + ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectclass=*)"), + []string{"dn", "displayname", "description"}, + nil) + + sr, err = login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + sort.Sort(EntryList(sr.Entries)) + + childrenOU := []Child{} + childrenOther := []Child{} + for _, item := range sr.Entries { + name := item.GetAttributeValue("displayname") + if name == "" { + name = item.GetAttributeValue("description") + } + child := Child{ + DN: item.DN, + Identifier: strings.Split(item.DN, ",")[0], + Name: name, + } + if strings.HasPrefix(item.DN, "ou=") { + childrenOU = append(childrenOU, child) + } else { + childrenOther = append(childrenOther, child) + } + } + + // Run template, finally! + templateAdminLDAP.Execute(w, &AdminLDAPTplData{ + DN: dn, + + Path: path, + ChildrenOU: childrenOU, + 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, + PossibleNewMembers: possibleNewMembers, + HasGroups: len(groups) > 0 || hasGroups, + Groups: groups, + PossibleNewGroups: possibleNewGroups, + + Common: NestedCommonTplData{ + CanAdmin: true, + LoggedIn: true, + Error: dError, + Success: dSuccess, + }, + }) +} + +func HandleAdminCreate(w http.ResponseWriter, r *http.Request) { + templateAdminCreate := getTemplate("admin/create.html") + + login := checkAdminLogin(w, r) + if login == nil { + return + } + + template := mux.Vars(r)["template"] + super_dn := mux.Vars(r)["super_dn"] + + // Check that base DN exists + searchRequest := ldap.NewSearchRequest( + super_dn, + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectclass=*)"), + []string{}, + 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("Parent object %s does not exist", super_dn), http.StatusNotFound) + return + } + + // Build path + path := []PathItem{ + PathItem{ + DN: config.BaseDN, + Identifier: config.BaseDN, + }, + } + + 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], + }) + } + + // Handle data + data := &CreateData{ + SuperDN: super_dn, + Path: path, + } + data.Template = template + if template == "user" { + data.IdType = config.UserNameAttr + data.StructuralObjectClass = "inetOrgPerson" + data.ObjectClass = "inetOrgPerson\norganizationalPerson\nperson\ntop" + } else if template == "group" || template == "ml" { + data.IdType = config.UserNameAttr + data.StructuralObjectClass = "groupOfNames" + data.ObjectClass = "groupOfNames\ntop" + data.Member = "cn=sogo@resdigita.org,ou=users,dc=resdigita,dc=org" + } else if template == "ou" { + data.IdType = "ou" + data.StructuralObjectClass = "organizationalUnit" + data.ObjectClass = "organizationalUnit\ntop" + } else { + data.IdType = "cn" + data.ObjectClass = "top" + data.Template = "" + } + + if r.Method == "POST" { + r.ParseForm() + if data.Template == "" { + data.IdType = strings.TrimSpace(strings.Join(r.Form["idtype"], "")) + data.StructuralObjectClass = strings.TrimSpace(strings.Join(r.Form["soc"], "")) + data.ObjectClass = strings.Join(r.Form["oc"], "") + } + 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"], "")) + data.OtherMailbox = strings.TrimSpace(strings.Join(r.Form["othermailbox"], "")) + + object_class := []string{} + for _, oc := range strings.Split(data.ObjectClass, "\n") { + x := strings.TrimSpace(oc) + if x != "" { + object_class = append(object_class, x) + } + } + + if len(object_class) == 0 { + data.Common.Error = "No object class specified" + } else if match, err := regexp.MatchString("^[a-z]+$", data.IdType); err != nil || !match { + data.Common.Error = "Invalid identifier type" + } else if len(data.IdValue) == 0 { + data.Common.Error = "No identifier specified" + } else { + newUser := models.User{ + DN: data.IdType + "=" + data.IdValue + "," + super_dn, + } + // 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.Mail != "" { + newUser.Mail = data.Mail + // req.Attribute("mail", []string{data.Mail}) + } + if data.IdType == "cn" { + newUser.CN = data.IdValue + } else if data.IdType == "mail" { + newUser.Mail = data.IdValue + } else if data.IdType == "uid" { + newUser.UID = data.IdValue + } + + if data.DisplayName != "" { + newUser.DisplayName = data.DisplayName + // req.Attribute("displayname", []string{data.DisplayName}) + } + if data.GivenName != "" { + newUser.GivenName = data.GivenName + // req.Attribute("givenname", []string{data.GivenName}) + } + + // if data.Member != "" { + // req.Attribute("member", []string{data.Member}) + // } + if data.SN != "" { + newUser.SN = data.SN + // req.Attribute("sn", []string{data.SN}) + } + if data.OtherMailbox != "" { + newUser.OtherMailbox = data.OtherMailbox + } + if data.Description != "" { + newUser.Description = data.Description + // req.Attribute("description", []string{data.Description}) + } + + models.AddUser(newUser, &config, login.conn) + + // 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.Common.Error = err.Error() + // } else { + if template == "ml" { + http.Redirect(w, r, "/admin/mailing/"+data.IdValue, http.StatusFound) + } else { + http.Redirect(w, r, "/admin/ldap/"+newUser.DN, http.StatusFound) + } + // } + } + } + data.Common.CanAdmin = true + + templateAdminCreate.Execute(w, data) +} diff --git a/views/directory.go b/views/directory.go new file mode 100644 index 0000000..9eafd9f --- /dev/null +++ b/views/directory.go @@ -0,0 +1,58 @@ +package views + +import ( + "guichet/models" + "html/template" + "net/http" + + // "sort" + "strings" + // "golang.org/x/crypto/openpgp/errors" + // "honnef.co/go/tools/analysis/facts/nilness" + // "github.com/go-ldap/ldap/v3" +) + +const FIELD_NAME_PROFILE_PICTURE = "profilePicture" +const FIELD_NAME_DIRECTORY_VISIBILITY = "directoryVisibility" + +func HandleDirectory(w http.ResponseWriter, r *http.Request) { + templateDirectory := getTemplate("directory.html") + + login := checkLogin(w, r) + if login == nil { + return + } + + templateDirectory.Execute(w, nil) +} + +func HandleDirectorySearch(w http.ResponseWriter, r *http.Request) error { + templateDirectoryResults := template.Must(template.ParseFiles(templatePath + "/directory_results.html")) + + //Get input value by user + r.ParseMultipartForm(1024) + input := strings.TrimSpace(strings.Join(r.Form["query"], "")) + + if r.Method != "POST" { + http.Error(w, "Invalid request", http.StatusBadRequest) + return nil + } + + //Log to allow the research + login := checkLogin(w, r) + if login == nil { + http.Error(w, "Login required", http.StatusUnauthorized) + return nil + } + + + search_results, err := models.DoDirectorySearch(login.conn, input) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return err + } + + templateDirectoryResults.Execute(w, search_results) + return nil + +} \ No newline at end of file diff --git a/views/home.go b/views/home.go new file mode 100644 index 0000000..f868f05 --- /dev/null +++ b/views/home.go @@ -0,0 +1,43 @@ +/* +home show the home page +*/ + +package views + +import ( + "net/http" +) + +func HandleHome(w http.ResponseWriter, r *http.Request) { + templateHome := getTemplate("home.html") + + login := checkLogin(w, r) + if login == nil { + status, _ := HandleLogin(w, r) + if status == nil { + return + } + login = checkLogin(w, r) + } + + can_admin := false + if login != nil { + can_admin = login.Common.CanAdmin + } + + data := HomePageData{ + Login: NestedLoginTplData{ + Login: login, + }, + BaseDN: config.BaseDN, + Org: config.Org, + Common: NestedCommonTplData{ + CanAdmin: can_admin, + CanInvite: true, + LoggedIn: true, + }, + } + execTemplate(w, templateHome, data.Common, data.Login, data) + // templateHome.Execute(w, data) + +} diff --git a/views/http.go b/views/http.go new file mode 100644 index 0000000..8bbdbf7 --- /dev/null +++ b/views/http.go @@ -0,0 +1,18 @@ +/* +http-utils provide utility functions that interact with http +*/ + +package views + +import ( + "net/http" +) + +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) + Handler.ServeHTTP(w, r) + }) +} + + diff --git a/views/invite.go b/views/invite.go new file mode 100644 index 0000000..8d9c2f5 --- /dev/null +++ b/views/invite.go @@ -0,0 +1,430 @@ +package views + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "encoding/hex" + "fmt" + "guichet/models" + "guichet/utils" + "html/template" + "log" + "net/http" + "regexp" + "strings" + + // "crypto/rand" + + // "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 +} + + + +func HandleInviteNewAccount(w http.ResponseWriter, r *http.Request) { + l, err := ldapOpen(w) + if err != nil { + log.Printf("view-invite.go - HandleInviteNewAccount - ldapOpen : %v", err) + log.Printf("view-invite.go - HandleInviteNewAccount - ldapOpen: %v", l) + } + if l == nil { + return + } + + err = l.Bind(config.NewUserDN, config.NewUserPassword) + if err != nil { + log.Printf("view-invite.go - HandleInviteNewAccount - l.Bind : %v", err) + log.Printf("view-invite.go - HandleInviteNewAccount - l.Bind: %v", config.NewUserDN) + panic(fmt.Sprintf("view-invite.go - HandleInviteNewAccount - l.Bind : %v", err)) + } + 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) + login := checkLogin(w, r) + inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN + err := login.conn.Bind(inviteDn, code_pw) + if err != nil { + templateInviteInvalidCode := getTemplate("user/code/invalid.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 +func HandleNewAccount(w http.ResponseWriter, r *http.Request, l *ldap.Conn, invitedBy string) bool { + templateInviteNewAccount := getTemplate("user/new.html") + data := NewAccountData{ + NewUserDefaultDomain: config.NewUserDefaultDomain, + } + if r.Method == "POST" { + r.ParseForm() + newUser := models.User{} + 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.OtherMailbox = strings.TrimSpace(strings.Join(r.Form["othermailbox"], "")) + newUser.Mail = strings.TrimSpace(strings.Join(r.Form["mail"], "")) + newUser.UID = strings.TrimSpace(strings.Join(r.Form["username"], "")) + newUser.CN = strings.TrimSpace(strings.Join(r.Form["username"], "")) + newUser.DN = "cn=" + strings.TrimSpace(strings.Join(r.Form["username"], "")) + "," + config.UserBaseDN + password1 := strings.Join(r.Form["password"], "") + password2 := strings.Join(r.Form["password2"], "") + if password1 != password2 { + data.Common.Success = false + data.ErrorPasswordMismatch = true + } else { + newUser.Password = password2 + l.Bind(config.NewUserDN, config.NewUserPassword) + err := models.AddUser(newUser, &config, l) + if err != nil { + data.Common.Success = false + data.Common.ErrorMessage = err.Error() + } + http.Redirect(w, r, "/user/wait", http.StatusFound) + } + // tryCreateAccount(l, data, password1, password2, invitedBy) + } else { + data.SuggestPW = fmt.Sprintf("%s", models.SuggestPassword()) + } + data.Common.CanAdmin = false + data.Common.LoggedIn = false + + templateInviteNewAccount.Execute(w, data) + return data.Common.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.Common.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 := utils.SSHAEncode(pass1) + if err != nil { + data.Common.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.Common.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.Common.WarningMessage += fmt.Sprintf("Cannot add to %s: %s\n", group, err.Error()) + } + } + data.Common.Success = true +} + +// ---- Code generation ---- +func HandleInviteSendCode(w http.ResponseWriter, r *http.Request) { + templateInviteSendCode := getTemplate("user/code/send.html") + login := checkInviterLogin(w, r) + if login == nil { + return + } + 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("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.Common.ErrorMessage = err.Error() + // // return + // } else { + // data.Common.Success = true + // data.CodeDisplay = code + // } + log.Printf("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("285: %v %v %v", code, code_id, code_pw) + log.Printf("286: %v", addReq) + err := login.conn.Add(addReq) + if err != nil { + data.Common.ErrorMessage = err.Error() + // return + } else { + data.Common.Success = true + data.CodeDisplay = code + } + data.Common.CanAdmin = login.Common.CanAdmin + templateInviteSendCode.Execute(w, data) + // if choice == "display" || choice == "send" { + // log.Printf("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("269: %v %v %v %v", login, choice, sendto, data) + // Generate code + code, code_id, code_pw := genCode() + log.Printf("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("899: %v",err) + // // log.Printf("899: %v",req) + // // log.Printf("899: %v",data) + // if err != nil { + // data.Common.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.Common.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("286: %v", req) + // data.Common.ErrorMessage = err.Error() + // return + // } + + // If we want to display it, do so + if choice == "display" { + data.Common.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, &models.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.Common.ErrorMessage = err.Error() + // return + // } + // log.Printf("Mail sent.") + data.Common.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("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 +} diff --git a/views/login.go b/views/login.go new file mode 100644 index 0000000..290d38b --- /dev/null +++ b/views/login.go @@ -0,0 +1,144 @@ +/* +login Handles login and current-user verification +*/ + +package views + +import ( + "fmt" + "log" + "net/http" + "strings" + + "github.com/go-ldap/ldap/v3" +) + + + + +func HandleLogout(w http.ResponseWriter, r *http.Request) { + + err := logout(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/", http.StatusFound) +} + +func HandleLogin(w http.ResponseWriter, r *http.Request) (*LoginInfo, error) { + templateLogin := getTemplate("login.html") + + if r.Method == "POST" { + // log.Printf("%v", "Parsing Form HandleLogin") + r.ParseForm() + + username := strings.Join(r.Form["username"], "") + password := strings.Join(r.Form["password"], "") + l, _ := ldapOpen(w) + + user_dn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, username, config.UserBaseDN) + + // log.Printf("%v", user_dn) + // log.Printf("%v", username) + + if strings.EqualFold(username, config.AdminAccount) { + user_dn = username + } + + + err := l.Bind(user_dn, password) + if err != nil { + log.Printf("DoLogin : %v", err) + log.Printf("DoLogin : %v", user_dn) + return nil, err + } + + + + + + + + // func encodePassword(inPassword string) (string, error) { + // utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) + // return utf16.NewEncoder().String("\"" + inPassword + "\"") + // // if err != nil { + // // log.Printf("Error encoding password: %s", err) + // // return err + // // } + + // } + + + // log.Printf("%v", LoginInfo) + if err != nil { + data := &LoginFormData{ + Username: username, + Common: NestedCommonTplData{ + CanAdmin: false, + CanInvite: true, + LoggedIn: false, + }, + } + if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) { + data.WrongPass = true + } else if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) { + data.WrongUser = true + } else { + log.Printf("%v", err) + log.Printf("%v", user_dn) + log.Printf("%v", username) + data.Common.ErrorMessage = err.Error() + } + } + // Successfully logged in, save it to session + session, err := GuichetSessionStore.Get(r, SESSION_NAME) + if err != nil { + session, _ = GuichetSessionStore.New(r, SESSION_NAME) + } + session.Values["login_username"] = username + session.Values["login_password"] = password + session.Values["login_dn"] = user_dn + + err = session.Save(r, w) + if err != nil { + log.Printf("DoLogin Session Save: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return nil, err + } + + + // // templateLogin.Execute(w, data) + // execTemplate(w, templateLogin, data.Common, NestedLoginTplData{}, *config, data) + + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + + } else if r.Method == "GET" { + execTemplate(w, templateLogin, NestedCommonTplData{ + CanAdmin: false, + CanInvite: true, + LoggedIn: false}, NestedLoginTplData{}, LoginFormData{ + Common: NestedCommonTplData{ + CanAdmin: false, + CanInvite: true, + LoggedIn: false}}) + // templateLogin.Execute(w, LoginFormData{ + // Common: NestedCommonTplData{ + // CanAdmin: false, + // CanInvite: true, + // LoggedIn: false}}) + return nil, nil + } else { + http.Error(w, "Unsupported method", http.StatusBadRequest) + return nil, nil + } + // execTemplate(w, templateLogin, data.Common, NestedLoginTplData{}, *config, data) + return nil, nil +} + +// func NotDoLogin(w http.ResponseWriter, r *http.Request, username string, user_dn string, password string) (*LoginInfo, error) { + + +// } diff --git a/views/passwd.go b/views/passwd.go new file mode 100644 index 0000000..ef7e41f --- /dev/null +++ b/views/passwd.go @@ -0,0 +1,161 @@ +package views + +import ( + b64 "encoding/base64" + "fmt" + "guichet/models" + "log" + "net/http" + "strings" + + // "github.com/go-ldap/ldap/v3" + "github.com/gorilla/mux" +) + +func HandleLostPassword(w http.ResponseWriter, r *http.Request) { + templateLostPasswordPage := getTemplate("passwd/lost.html") + if checkLogin(w, r) != nil { + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + } + + data := PasswordLostData{ + Common: NestedCommonTplData{ + 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 := models.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"], "")), + } + ldapNewConn, err := models.OpenNewUserLdap(&config) + if err != nil { + log.Printf(fmt.Sprintf("HandleLostPassword 99 : %v %v", err, ldapNewConn)) + data.Common.ErrorMessage = err.Error() + } + if err != nil { + log.Printf(fmt.Sprintf("HandleLostPassword 104 : %v %v", err, ldapNewConn)) + data.Common.ErrorMessage = err.Error() + } else { + // err = ldapConn.Bind(config.NewUserDN, config.NewUserPassword) + if err != nil { + log.Printf(fmt.Sprintf("HandleLostPassword 109 : %v %v", err, ldapNewConn)) + data.Common.ErrorMessage = err.Error() + } else { + data.Common.Success = true + } + } + err = models.PasswordLost(user, &config, ldapNewConn) + } + data.Common.CanAdmin = false + // templateLostPasswordPage.Execute(w, data) + execTemplate(w, templateLostPasswordPage, data.Common, NestedLoginTplData{}, data) +} + +func HandleFoundPassword(w http.ResponseWriter, r *http.Request) { + templateFoundPasswordPage := getTemplate("passwd.html") + data := PasswdTplData{ + Common: NestedCommonTplData{ + CanAdmin: false, + LoggedIn: false}, + } + code := mux.Vars(r)["code"] + // code = strings.TrimSpace(strings.Join([]string{code}, "")) + newCode, _ := b64.URLEncoding.DecodeString(code) + ldapNewConn, err := models.OpenNewUserLdap(&config) + if err != nil { + log.Printf("HandleFoundPassword OpenNewUserLdap(config) : %v", err) + data.Common.ErrorMessage = err.Error() + } + codeArray := strings.Split(string(newCode), ";") + user := models.User{ + UID: codeArray[0], + Password: codeArray[1], + DN: "uid=" + codeArray[0] + "," + config.InvitationBaseDN, + } + user.SeeAlso, err = models.PasswordFound(user, &config, ldapNewConn) + if err != nil { + log.Printf("PasswordFound(models.User, config, ldapConn) %v", err) + log.Printf("PasswordFound(models.User, config, ldapConn) %v", user) + log.Printf("PasswordFound(models.User, config, ldapConn) %v", ldapNewConn) + data.Common.ErrorMessage = err.Error() + } + if r.Method == "POST" { + r.ParseForm() + + password := strings.Join(r.Form["password"], "") + password2 := strings.Join(r.Form["password2"], "") + + if len(password) < 8 { + data.TooShortError = true + } else if password2 != password { + data.NoMatchError = true + } else { + err := models.PassWD(models.User{ + DN: user.SeeAlso, + Password: password, + }, &config, ldapNewConn) + if err != nil { + data.Common.ErrorMessage = err.Error() + } else { + data.Common.Success = true + } + } + } + data.Common.CanAdmin = false + // templateFoundPasswordPage.Execute(w, data) + execTemplate(w, templateFoundPasswordPage, data.Common, data.Login, data) +} + +func HandlePasswd(w http.ResponseWriter, r *http.Request) { + templatePasswd := getTemplate("passwd.html") + data := &PasswdTplData{ + Common: NestedCommonTplData{ + CanAdmin: false, + LoggedIn: true, + ErrorMessage: "", + Success: false, + }, + } + + login := checkLogin(w, r) + if login == nil { + http.Redirect(w, r, "/", http.StatusFound) + return + } + data.Login.Status = login + data.Common.CanAdmin = login.Common.CanAdmin + + if r.Method == "POST" { + r.ParseForm() + + password := strings.Join(r.Form["password"], "") + password2 := strings.Join(r.Form["password2"], "") + + if len(password) < 8 { + data.TooShortError = true + } else if password2 != password { + data.NoMatchError = true + } else { + err := models.PassWD(models.User{ + DN: login.Info.DN, + Password: password, + }, &config, login.conn) + if err != nil { + data.Common.ErrorMessage = err.Error() + } else { + data.Common.Success = true + } + } + } + data.Common.CanAdmin = false + // templatePasswd.Execute(w, data) + execTemplate(w, templatePasswd, data.Common, data.Login, data) +} diff --git a/views/picture.go b/views/picture.go new file mode 100644 index 0000000..87b9614 --- /dev/null +++ b/views/picture.go @@ -0,0 +1,184 @@ +package views + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "strconv" + + "image" + "image/jpeg" + _ "image/png" + + "mime/multipart" + "net/http" + "strings" + + "github.com/google/uuid" + "github.com/gorilla/mux" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/nfnt/resize" +) + +func newMinioClient() (*minio.Client, error) { + endpoint := config.S3Endpoint + accessKeyID := config.S3AccessKey + secretKeyID := config.S3SecretKey + useSSL := true + + //Initialize Minio + minioCLient, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKeyID, secretKeyID, ""), + Secure: useSSL, + Region: config.S3Region, + }) + + if err != nil { + return nil, err + } + + return minioCLient, nil +} + +// Upload image through guichet server. +func uploadProfilePicture(w http.ResponseWriter, r *http.Request, login *LoginStatus) (string, error) { + file, _, err := r.FormFile("image") + + if err == http.ErrMissingFile { + return "", nil + } + if err != nil { + return "", err + } + defer file.Close() + + err = checkImage(file) + if err != nil { + return "", err + } + + buffFull := bytes.NewBuffer([]byte{}) + buffThumb := bytes.NewBuffer([]byte{}) + err = resizePicture(file, buffFull, buffThumb) + if err != nil { + return "", err + } + + mc, err := newMinioClient() + if err != nil || mc == nil { + return "", err + } + + // If a previous profile picture existed, delete it + // (don't care about errors) + if nameConsul := login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE); nameConsul != "" { + mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul, minio.RemoveObjectOptions{}) + mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul+"-thumb", minio.RemoveObjectOptions{}) + } + + // Generate new random name for picture + nameFull := uuid.New().String() + nameThumb := nameFull + "-thumb" + + _, err = mc.PutObject(context.Background(), config.S3Bucket, nameThumb, buffThumb, int64(buffThumb.Len()), minio.PutObjectOptions{ + ContentType: "image/jpeg", + }) + if err != nil { + return "", err + } + + _, err = mc.PutObject(context.Background(), config.S3Bucket, nameFull, buffFull, int64(buffFull.Len()), minio.PutObjectOptions{ + ContentType: "image/jpeg", + }) + if err != nil { + return "", err + } + + return nameFull, nil +} + +func checkImage(file multipart.File) error { + buff := make([]byte, 512) //Detect read only the first 512 bytes + _, err := file.Read(buff) + if err != nil { + return err + } + file.Seek(0, 0) + + fileType := http.DetectContentType(buff) + fileType = strings.Split(fileType, "/")[0] + if fileType != "image" { + return errors.New("bad type") + } + + return nil +} + +func resizePicture(file multipart.File, buffFull, buffThumb *bytes.Buffer) error { + file.Seek(0, 0) + picture, _, err := image.Decode(file) + if err != nil { + return err + } + + thumbnail := resize.Thumbnail(90, 90, picture, resize.Lanczos3) + picture = resize.Thumbnail(480, 480, picture, resize.Lanczos3) + + err = jpeg.Encode(buffFull, picture, &jpeg.Options{ + Quality: 95, + }) + if err != nil { + return err + } + + err = jpeg.Encode(buffThumb, thumbnail, &jpeg.Options{ + Quality: 100, + }) + + return err +} + +func HandleDownloadPicture(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + + //Check login + login := checkLogin(w, r) + if login == nil { + return + } + + //Get the object after connect MC + mc, err := newMinioClient() + if err != nil { + http.Error(w, "MinioClient: "+err.Error(), http.StatusInternalServerError) + return + } + + obj, err := mc.GetObject(context.Background(), "bottin-pictures", name, minio.GetObjectOptions{}) + if err != nil { + http.Error(w, "MinioClient: GetObject: "+err.Error(), http.StatusInternalServerError) + return + } + defer obj.Close() + + objStat, err := obj.Stat() + if err != nil { + http.Error(w, "MiniObjet: "+err.Error(), http.StatusInternalServerError) + return + } + + //Send JSON through xhttp + w.Header().Set("Content-Type", objStat.ContentType) + w.Header().Set("Content-Length", strconv.Itoa(int(objStat.Size))) + //Copy obj in w + writting, err := io.Copy(w, obj) + + if writting != objStat.Size || err != nil { + http.Error(w, fmt.Sprintf("WriteBody: %s, bytes wrote %d on %d", err.Error(), writting, objStat.Size), http.StatusInternalServerError) + return + } + +} diff --git a/views/session.go b/views/session.go new file mode 100644 index 0000000..e66f013 --- /dev/null +++ b/views/session.go @@ -0,0 +1,177 @@ +/* +Handles session login and lougout with HTTP stuff +*/ +package views + +import ( + "guichet/models" + "log" + "net/http" +) + +func checkLogin(w http.ResponseWriter, r *http.Request) *LoginStatus { + var login_info *LoginInfo + l, err := ldapOpen(w) + if l == nil { + return nil + } + session, err := GuichetSessionStore.Get(r, SESSION_NAME) + if err != nil { + log.Printf("checkLogin ldapOpen : %v", err) + log.Printf("checkLogin ldapOpen : %v", session) + log.Printf("checkLogin ldapOpen : %v", session.Values) + return nil + } + username, ok := session.Values["login_username"] + password, ok2 := session.Values["login_password"] + user_dn, ok3 := session.Values["login_dn"] + + if ok && ok2 && ok3 { + login_info = &LoginInfo{ + DN: user_dn.(string), + Username: username.(string), + Password: password.(string), + } + err = models.Bind(models.User{ + DN: login_info.DN, + Password: login_info.Password, + }, &config, l) + if err != nil { + delete(session.Values, "login_username") + delete(session.Values, "login_password") + delete(session.Values, "login_dn") + + err = session.Save(r, w) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return nil + } + return checkLogin(w, r) + } + ldapUser, err := models.GetUser(models.User{ + DN: login_info.DN, + CN: login_info.Username, + }, &config, l) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return nil + } + userEntry := ldapUser.UserEntry + loginStatus :=LoginStatus{ + Info: login_info, + conn: l, + UserEntry: userEntry, + Common: NestedCommonTplData{ + CanAdmin: ldapUser.CanAdmin, + CanInvite: ldapUser.CanInvite, + }, + } + return &loginStatus + } else { + return nil + } +} + +/* + + requestKind := "(objectClass=organizationalPerson)" + if strings.EqualFold(login_info.DN, config.AdminAccount) { + requestKind = "(objectclass=*)" + } + searchRequest := ldap.NewSearchRequest( + login_info.DN, + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, + requestKind, + []string{ + "dn", + "displayname", + "givenname", + "sn", + "mail", + "cn", + "memberof", + "description", + "garage_s3_access_key", + }, + nil) + // FIELD_NAME_DIRECTORY_VISIBILITY, + // FIELD_NAME_PROFILE_PICTURE, + + sr, err := l.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return nil + } + + if len(sr.Entries) != 1 { + http.Error(w, fmt.Sprintf("Unable to find entry for %s", login_info.DN), http.StatusInternalServerError) + return nil + } + + loginStatus.UserEntry = sr.Entries[0] + + loginStatus.CanAdmin = strings.EqualFold(loginStatus.Info.DN, config.AdminAccount) + loginStatus.CanInvite = false + + groups := []EntryName{} + searchRequest = ldap.NewSearchRequest( + config.GroupBaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=groupOfNames)(member=%s))", login_info.DN), + []string{"dn", "displayName", "cn", "description"}, + nil) + // // log.Printf(fmt.Sprintf("708: %v",searchRequest)) + sr, err = l.Search(searchRequest) + // if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + // } + //// log.Printf(fmt.Sprintf("303: %v",sr.Entries)) + for _, ent := range sr.Entries { + // log.Printf(fmt.Sprintf("305: %v",ent.DN)) + groups = append(groups, EntryName{ + DN: ent.DN, + Name: ent.GetAttributeValue("cn"), + }) + // log.Printf(fmt.Sprintf("310: %v",config.GroupCanInvite)) + if config.GroupCanInvite != "" && strings.EqualFold(ent.DN, config.GroupCanInvite) { + loginStatus.CanInvite = true + } + // log.Printf(fmt.Sprintf("314: %v",config.GroupCanAdmin)) + if config.GroupCanAdmin != "" && strings.EqualFold(ent.DN, config.GroupCanAdmin) { + loginStatus.CanAdmin = true + } + } + + // for _, attr := range loginStatus.UserEntry.Attributes { + // if strings.EqualFold(attr.Name, "memberof") { + // for _, group := range attr.Values { + // if config.GroupCanInvite != "" && strings.EqualFold(group, config.GroupCanInvite) { + // loginStatus.CanInvite = true + // } + // if config.GroupCanAdmin != "" && strings.EqualFold(group, config.GroupCanAdmin) { + // loginStatus.CanAdmin = true + // } + // } + // } + // } + + return loginStatus +*/ + +func logout(w http.ResponseWriter, r *http.Request) error { + session, err := GuichetSessionStore.Get(r, SESSION_NAME) + if err != nil { + session, _ = GuichetSessionStore.New(r, SESSION_NAME) + // return err + } else { + delete(session.Values, "login_username") + delete(session.Values, "login_password") + delete(session.Values, "login_dn") + + err = session.Save(r, w) + } + + // return err + return nil +} diff --git a/views/user.go b/views/user.go new file mode 100644 index 0000000..d319ad8 --- /dev/null +++ b/views/user.go @@ -0,0 +1,175 @@ +package views + +import ( + // b64 "encoding/base64" + "fmt" + // "log" + "guichet/models" + "log" + "net/http" + "strings" + + "github.com/go-ldap/ldap/v3" + // "github.com/gorilla/mux" +) + +func HandleUserWait(w http.ResponseWriter, r *http.Request) { + templateUser := getTemplate("user/wait.html") + templateUser.Execute(w, HomePageData{ + Common: NestedCommonTplData{ + CanAdmin: false, + LoggedIn: false, + }, + }) +} + +func HandleUserMail(w http.ResponseWriter, r *http.Request) { + login := checkLogin(w, r) + if login == nil { + http.Redirect(w, r, "/", http.StatusFound) + return + } + email := r.FormValue("email") + action := r.FormValue("action") + var err error + if action == "Add" { + // Add the new mail value to the entry + modifyRequest := ldap.NewModifyRequest(login.Info.DN, nil) + modifyRequest.Add("mail", []string{email}) + + err = login.conn.Modify(modifyRequest) + if err != nil { + http.Error(w, fmt.Sprintf("Error adding the email: %v", modifyRequest), http.StatusInternalServerError) + return + } + } else if action == "Delete" { + modifyRequest := ldap.NewModifyRequest(login.Info.DN, nil) + modifyRequest.Delete("mail", []string{email}) + + log.Printf("HandleUserMail %v", modifyRequest) + err = login.conn.Modify(modifyRequest) + if err != nil { + log.Printf("HandleUserMail DeleteMail %v", err) + http.Error(w, fmt.Sprintf("Error deleting the email: %s", err), http.StatusInternalServerError) + return + } + } + + message := fmt.Sprintf("Mail value updated successfully to: %s", email) + http.Redirect(w, r, "/user?message="+message, http.StatusSeeOther) + +} + +func toInteger(index string) { + panic("unimplemented") +} + +func HandleUser(w http.ResponseWriter, r *http.Request) { + templateUser := getTemplate("user.html") + + login := checkLogin(w, r) + if login == nil { + http.Redirect(w, r, "/", http.StatusFound) + return + } + + data := &ProfileTplData{ + Login: NestedLoginTplData{ + Status: login, + Login: login, + }, + Common: NestedCommonTplData{ + CanAdmin: login.Common.CanAdmin, + LoggedIn: true, + ErrorMessage: "", + Success: false, + }, + } + + data.Mail = login.UserEntry.GetAttributeValue("mail") + data.DisplayName = login.UserEntry.GetAttributeValue("displayName") + data.GivenName = login.UserEntry.GetAttributeValue("givenName") + data.Surname = login.UserEntry.GetAttributeValue("sn") + data.OtherMailbox = login.UserEntry.GetAttributeValue("carLicense") + data.MailValues = login.UserEntry.GetAttributeValues("mail") + // data.Visibility = login.UserEntry.GetAttributeValue(FIELD_NAME_DIRECTORY_VISIBILITY) + data.Description = login.UserEntry.GetAttributeValue("description") + //data.ProfilePicture = login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE) + + if r.Method == "POST" { + //5MB maximum size files + r.ParseMultipartForm(5 << 20) + user := models.User{ + DN: login.Info.DN, + GivenName: strings.TrimSpace(strings.Join(r.Form["given_name"], "")), + DisplayName: strings.TrimSpace(strings.Join(r.Form["display_name"], "")), + Mail: strings.TrimSpace(strings.Join(r.Form["mail"], "")), + SN: strings.TrimSpace(strings.Join(r.Form["surname"], "")), + OtherMailbox: strings.TrimSpace(strings.Join(r.Form["othermailbox"], "")), + Description: strings.TrimSpace(strings.Join(r.Form["description"], "")), + // Password: , + //UID: , + // CN: , + } + + if user.DisplayName != "" { + err := models.ModifyUser(user, &config, login.conn) + if err != nil { + data.Common.ErrorMessage = "HandleUser : " + err.Error() + } else { + data.Common.Success = true + } + } + findUser, err := models.GetUser(user, &config, login.conn) + if err != nil { + data.Common.ErrorMessage = "HandleUser : " + err.Error() + } + data.DisplayName = findUser.DisplayName + data.GivenName = findUser.GivenName + data.Surname = findUser.SN + data.Description = findUser.Description + data.Mail = findUser.Mail + data.Common.LoggedIn = false + + /* + visible := strings.TrimSpace(strings.Join(r.Form["visibility"], "")) + if visible != "" { + visible = "on" + } else { + visible = "off" + } + data.Visibility = visible + */ + /* + profilePicture, err := uploadProfilePicture(w, r, login) + if err != nil { + data.Common.ErrorMessage = err.Error() + } + if profilePicture != "" { + data.ProfilePicture = profilePicture + } + */ + + //modify_request.Replace(FIELD_NAME_DIRECTORY_VISIBILITY, []string{data.Visibility}) + //modify_request.Replace(FIELD_NAME_DIRECTORY_VISIBILITY, []string{"on"}) + //if data.ProfilePicture != "" { + // modify_request.Replace(FIELD_NAME_PROFILE_PICTURE, []string{data.ProfilePicture}) + // } + + // err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("Profile:079: %v",modify_request)) + // log.Printf(fmt.Sprintf("Profile:079: %v",err)) + // log.Printf(fmt.Sprintf("Profile:079: %v",data)) + // if err != nil { + // data.Common.ErrorMessage = err.Error() + // } else { + // data.Common.Success = true + // } + + } + + log.Printf("HandleUser : %v", data) + + // templateUser.Execute(w, data) + execTemplate(w, templateUser, data.Common, data.Login, data) +} diff --git a/views/view.go b/views/view.go new file mode 100644 index 0000000..14170ca --- /dev/null +++ b/views/view.go @@ -0,0 +1,368 @@ +/* +Creates the webpages to be processed by Guichet +*/ +package views + +import ( + "crypto/tls" + "encoding/json" + "guichet/models" + "io/ioutil" + "net" + + "flag" + "html/template" + "log" + "net/http" + "os" + + // "net/http" + "strings" + + "github.com/go-ldap/ldap/v3" + "github.com/gorilla/sessions" +) + +const SESSION_NAME = "guichet_session" + +var templatePath = "./templates" +var GuichetSessionStore sessions.Store = nil + +type EntryList []*ldap.Entry +type LoginInfo struct { + Username string + DN string + Password string +} +func ReadConfig() models.ConfigFile { + // Default configuration values for certain fields + flag.Parse() + var configFlag = flag.String("config", "./config.json", "Configuration file path") + + config_file := models.ConfigFile{ + HttpBindAddr: ":9991", + LdapServerAddr: "ldap://127.0.0.1:389", + + UserNameAttr: "uid", + GroupNameAttr: "gid", + + InvitationNameAttr: "cn", + InvitedAutoGroups: []string{}, + + Org: "ResDigita", + } + + _, err := os.Stat(*configFlag) + if os.IsNotExist(err) { + log.Fatalf("Could not find Guichet configuration file at %s. Please create this file, for exemple starting with config.json.exemple and customizing it for your deployment.", *configFlag) + } + + if err != nil { + log.Fatal(err) + } + + bytes, err := ioutil.ReadFile(*configFlag) + if err != nil { + log.Fatal(err) + } + + err = json.Unmarshal(bytes, &config_file) + if err != nil { + log.Fatal(err) + } + + return config_file +} +type LoginStatus struct { + Info *LoginInfo + conn *ldap.Conn + UserEntry *ldap.Entry + Common NestedCommonTplData +} +type NestedCommonTplData struct { + Error string + ErrorMessage string + CanAdmin bool + CanInvite bool + LoggedIn bool + Success bool + WarningMessage string + WebsiteName string + WebsiteURL string +} +type CodeMailFields struct { + From string + To string + Code string + InviteFrom string + WebBaseAddress string + Common NestedCommonTplData +} + +var config = ReadConfig() + +func ldapOpen(w http.ResponseWriter) (*ldap.Conn, error) { + if config.LdapTLS { + tlsConf := &tls.Config{ + ServerName: config.LdapServerAddr, + InsecureSkipVerify: true, + } + return ldap.DialTLS("tcp", net.JoinHostPort(config.LdapServerAddr, "636"), tlsConf) + } else { + return ldap.DialURL("ldap://" + config.LdapServerAddr) + } + + // if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // log.Printf(fmt.Sprintf("27: %v %v", err, l)) + // return nil + // } + + // return l +} + + +// type keyView struct { +// Status *LoginStatus +// Key *garage.KeyInfo +// } +// type webInspectView struct { +// Status *LoginStatus +// Key *garage.KeyInfo +// Bucket *garage.BucketInfo +// IndexDoc string +// ErrorDoc string +// MaxObjects int64 +// MaxSize int64 +// UsedSizePct float64 +// } +// type webListView struct { +// Status *LoginStatus +// Key *garage.KeyInfo +// } +type LayoutTemplateData struct { + Common NestedCommonTplData + Login NestedLoginTplData + Data any +} +type NestedLoginTplData struct { + Login *LoginStatus + Username string + Status *LoginStatus +} + + +func execTemplate(w http.ResponseWriter, t *template.Template, commonData NestedCommonTplData, loginData NestedLoginTplData, data any) error { + commonData.WebsiteURL = config.WebAddress + commonData.WebsiteName = config.Org + return t.Execute(w, LayoutTemplateData{ + Common: commonData, + Login: loginData, + Data: data, + }) +} + + +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 +} + + +type AdminUsersTplData struct { + UserNameAttr string + UserBaseDN string + Users EntryList + Common NestedCommonTplData + Login NestedLoginTplData +} +type AdminLDAPTplData struct { + DN string + + Path []PathItem + ChildrenOU []Child + ChildrenOther []Child + CanAddChild bool + Props map[string]*PropValues + CanDelete bool + + HasMembers bool + Members []EntryName + PossibleNewMembers []EntryName + HasGroups bool + Groups []EntryName + PossibleNewGroups []EntryName + + ListMemGro map[string]string + + Common NestedCommonTplData + Login NestedLoginTplData +} +type AdminMailingListTplData struct { + Common NestedCommonTplData + Login NestedLoginTplData + MailingNameAttr string + MailingBaseDN string + MailingList *ldap.Entry + Members EntryList + PossibleNewMembers EntryList + AllowGuest bool +} +type AdminMailingTplData struct { + Common NestedCommonTplData + Login NestedLoginTplData + MailingNameAttr string + MailingBaseDN string + MailingLists EntryList +} +type AdminGroupsTplData struct { + Common NestedCommonTplData + Login NestedLoginTplData + GroupNameAttr string + GroupBaseDN string + Groups EntryList +} +type EntryName struct { + DN string + Name string +} +type Child struct { + DN string + Identifier string + Name string +} +type PathItem struct { + DN string + Identifier string + Active bool +} +type PropValues struct { + Name string + Values []string + Editable bool + Deletable bool +} +type CreateData struct { + SuperDN string + Path []PathItem + Template string + + IdType string + IdValue string + DisplayName string + GivenName string + Member string + Mail string + Description string + StructuralObjectClass string + ObjectClass string + SN string + OtherMailbox string + + Common NestedCommonTplData + Login NestedLoginTplData +} + + +type HomePageData struct { + Common NestedCommonTplData + Login NestedLoginTplData + BaseDN string + Org string +} +type PasswordFoundData struct { + Common NestedCommonTplData + Login NestedLoginTplData + Username string + Mail string + OtherMailbox string +} +type PasswordLostData struct { + Common NestedCommonTplData + ErrorMessage string + Success bool + Username string + Mail string + OtherMailbox string +} +type NewAccountData struct { + Username string + DisplayName string + GivenName string + Surname string + Mail string + SuggestPW string + OtherMailbox string + + ErrorUsernameTaken bool + ErrorInvalidUsername bool + ErrorPasswordTooShort bool + ErrorPasswordMismatch bool + Common NestedCommonTplData + NewUserDefaultDomain string +} +type SendCodeData struct { + Common NestedCommonTplData + ErrorInvalidEmail bool + + CodeDisplay string + CodeSentTo string + WebBaseAddress string +} + + +type ProfileTplData struct { + Mail string + MailValues []string + DisplayName string + GivenName string + Surname string + Description string + OtherMailbox string + Common NestedCommonTplData + Login NestedLoginTplData +} + +//ProfilePicture string +//Visibility string + +type PasswdTplData struct { + Common NestedCommonTplData + Login NestedLoginTplData + TooShortError bool + NoMatchError bool +} + + +type LoginFormData struct { + Username string + WrongUser bool + WrongPass bool + Common NestedCommonTplData +} + + + +type WrapperTemplate struct { + Template *template.Template +} + + + +func getTemplate(name string) *template.Template { + return template.Must(template.New("layout.html").Funcs(template.FuncMap{ + "contains": strings.Contains, + }).ParseFiles( + templatePath+"/layout.html", + templatePath+"/"+name, + )) +} + + +