An API for Guichet #23

Merged
quentin merged 14 commits from api into main 2023-09-26 06:44:36 +00:00
11 changed files with 534 additions and 455 deletions
Showing only changes of commit c06f52837e - Show all commits

View file

@ -11,18 +11,18 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
func checkAdminLogin(w http.ResponseWriter, r *http.Request) *LoginStatus { func checkAdminLogin(w http.ResponseWriter, r *http.Request) *LoggedUser {
login := checkLogin(w, r) user := RequireUserHtml(w, r)
if login == nil { if user == nil {
return nil return nil
} }
if !login.CanAdmin { if !user.Capabilities.CanAdmin {
http.Error(w, "Not authorized to perform administrative operations.", http.StatusUnauthorized) http.Error(w, "Not authorized to perform administrative operations.", http.StatusUnauthorized)
return nil return nil
} }
return login return user
} }
type EntryList []*ldap.Entry type EntryList []*ldap.Entry
@ -40,7 +40,7 @@ func (d EntryList) Less(i, j int) bool {
} }
type AdminUsersTplData struct { type AdminUsersTplData struct {
Login *LoginStatus User *LoggedUser
UserNameAttr string UserNameAttr string
UserBaseDN string UserBaseDN string
Users EntryList Users EntryList
@ -49,8 +49,8 @@ type AdminUsersTplData struct {
func handleAdminUsers(w http.ResponseWriter, r *http.Request) { func handleAdminUsers(w http.ResponseWriter, r *http.Request) {
templateAdminUsers := getTemplate("admin_users.html") templateAdminUsers := getTemplate("admin_users.html")
login := checkAdminLogin(w, r) user := checkAdminLogin(w, r)
if login == nil { if user == nil {
return return
} }
@ -61,14 +61,14 @@ func handleAdminUsers(w http.ResponseWriter, r *http.Request) {
[]string{config.UserNameAttr, "dn", "displayname", "givenname", "sn", "mail"}, []string{config.UserNameAttr, "dn", "displayname", "givenname", "sn", "mail"},
nil) nil)
sr, err := login.conn.Search(searchRequest) sr, err := user.Login.conn.Search(searchRequest)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
data := &AdminUsersTplData{ data := &AdminUsersTplData{
Login: login, User: user,
UserNameAttr: config.UserNameAttr, UserNameAttr: config.UserNameAttr,
UserBaseDN: config.UserBaseDN, UserBaseDN: config.UserBaseDN,
Users: EntryList(sr.Entries), Users: EntryList(sr.Entries),
@ -79,7 +79,7 @@ func handleAdminUsers(w http.ResponseWriter, r *http.Request) {
} }
type AdminGroupsTplData struct { type AdminGroupsTplData struct {
Login *LoginStatus User *LoggedUser
GroupNameAttr string GroupNameAttr string
GroupBaseDN string GroupBaseDN string
Groups EntryList Groups EntryList
@ -88,8 +88,8 @@ type AdminGroupsTplData struct {
func handleAdminGroups(w http.ResponseWriter, r *http.Request) { func handleAdminGroups(w http.ResponseWriter, r *http.Request) {
templateAdminGroups := getTemplate("admin_groups.html") templateAdminGroups := getTemplate("admin_groups.html")
login := checkAdminLogin(w, r) user := checkAdminLogin(w, r)
if login == nil { if user == nil {
return return
} }
@ -100,14 +100,14 @@ func handleAdminGroups(w http.ResponseWriter, r *http.Request) {
[]string{config.GroupNameAttr, "dn", "description"}, []string{config.GroupNameAttr, "dn", "description"},
nil) nil)
sr, err := login.conn.Search(searchRequest) sr, err := user.Login.conn.Search(searchRequest)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
data := &AdminGroupsTplData{ data := &AdminGroupsTplData{
Login: login, User: user,
GroupNameAttr: config.GroupNameAttr, GroupNameAttr: config.GroupNameAttr,
GroupBaseDN: config.GroupBaseDN, GroupBaseDN: config.GroupBaseDN,
Groups: EntryList(sr.Entries), Groups: EntryList(sr.Entries),
@ -118,7 +118,7 @@ func handleAdminGroups(w http.ResponseWriter, r *http.Request) {
} }
type AdminMailingTplData struct { type AdminMailingTplData struct {
Login *LoginStatus User *LoggedUser
MailingNameAttr string MailingNameAttr string
MailingBaseDN string MailingBaseDN string
MailingLists EntryList MailingLists EntryList
@ -127,8 +127,8 @@ type AdminMailingTplData struct {
func handleAdminMailing(w http.ResponseWriter, r *http.Request) { func handleAdminMailing(w http.ResponseWriter, r *http.Request) {
templateAdminMailing := getTemplate("admin_mailing.html") templateAdminMailing := getTemplate("admin_mailing.html")
login := checkAdminLogin(w, r) user := checkAdminLogin(w, r)
if login == nil { if user == nil {
return return
} }
@ -139,14 +139,14 @@ func handleAdminMailing(w http.ResponseWriter, r *http.Request) {
[]string{config.MailingNameAttr, "dn", "description"}, []string{config.MailingNameAttr, "dn", "description"},
nil) nil)
sr, err := login.conn.Search(searchRequest) sr, err := user.Login.conn.Search(searchRequest)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
data := &AdminMailingTplData{ data := &AdminMailingTplData{
Login: login, User: user,
MailingNameAttr: config.MailingNameAttr, MailingNameAttr: config.MailingNameAttr,
MailingBaseDN: config.MailingBaseDN, MailingBaseDN: config.MailingBaseDN,
MailingLists: EntryList(sr.Entries), MailingLists: EntryList(sr.Entries),
@ -157,7 +157,7 @@ func handleAdminMailing(w http.ResponseWriter, r *http.Request) {
} }
type AdminMailingListTplData struct { type AdminMailingListTplData struct {
Login *LoginStatus User *LoggedUser
MailingNameAttr string MailingNameAttr string
MailingBaseDN string MailingBaseDN string
@ -173,8 +173,8 @@ type AdminMailingListTplData struct {
func handleAdminMailingList(w http.ResponseWriter, r *http.Request) { func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
templateAdminMailingList := getTemplate("admin_mailing_list.html") templateAdminMailingList := getTemplate("admin_mailing_list.html")
login := checkAdminLogin(w, r) user := checkAdminLogin(w, r)
if login == nil { if user == nil {
return return
} }
@ -193,7 +193,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
modify_request := ldap.NewModifyRequest(dn, nil) modify_request := ldap.NewModifyRequest(dn, nil)
modify_request.Add("member", []string{member}) modify_request.Add("member", []string{member})
err := login.conn.Modify(modify_request) err := user.Login.conn.Modify(modify_request)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
@ -209,7 +209,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
fmt.Sprintf("(&(objectClass=organizationalPerson)(mail=%s))", mail), fmt.Sprintf("(&(objectClass=organizationalPerson)(mail=%s))", mail),
[]string{"dn", "displayname", "mail"}, []string{"dn", "displayname", "mail"},
nil) nil)
sr, err := login.conn.Search(searchRequest) sr, err := user.Login.conn.Search(searchRequest)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
@ -222,14 +222,14 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
if displayname != "" { if displayname != "" {
req.Attribute("displayname", []string{displayname}) req.Attribute("displayname", []string{displayname})
} }
err := login.conn.Add(req) err := user.Login.conn.Add(req)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
modify_request := ldap.NewModifyRequest(dn, nil) modify_request := ldap.NewModifyRequest(dn, nil)
modify_request.Add("member", []string{guestDn}) modify_request.Add("member", []string{guestDn})
err := login.conn.Modify(modify_request) err := user.Login.conn.Modify(modify_request)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
@ -243,7 +243,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
modify_request := ldap.NewModifyRequest(dn, nil) modify_request := ldap.NewModifyRequest(dn, nil)
modify_request.Add("member", []string{sr.Entries[0].DN}) modify_request.Add("member", []string{sr.Entries[0].DN})
err := login.conn.Modify(modify_request) err := user.Login.conn.Modify(modify_request)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
@ -258,7 +258,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
modify_request := ldap.NewModifyRequest(dn, nil) modify_request := ldap.NewModifyRequest(dn, nil)
modify_request.Delete("member", []string{member}) modify_request.Delete("member", []string{member})
err := login.conn.Modify(modify_request) err := user.Login.conn.Modify(modify_request)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
@ -275,7 +275,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
[]string{"dn", config.MailingNameAttr, "member", "description"}, []string{"dn", config.MailingNameAttr, "member", "description"},
nil) nil)
sr, err := login.conn.Search(searchRequest) sr, err := user.Login.conn.Search(searchRequest)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -307,7 +307,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
fmt.Sprintf("(objectClass=organizationalPerson)"), fmt.Sprintf("(objectClass=organizationalPerson)"),
[]string{"dn", "displayname", "mail"}, []string{"dn", "displayname", "mail"},
nil) nil)
sr, err = login.conn.Search(searchRequest) sr, err = user.Login.conn.Search(searchRequest)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -322,7 +322,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
} }
data := &AdminMailingListTplData{ data := &AdminMailingListTplData{
Login: login, User: user,
MailingNameAttr: config.MailingNameAttr, MailingNameAttr: config.MailingNameAttr,
MailingBaseDN: config.MailingBaseDN, MailingBaseDN: config.MailingBaseDN,
@ -394,8 +394,8 @@ type PropValues struct {
func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
templateAdminLDAP := getTemplate("admin_ldap.html") templateAdminLDAP := getTemplate("admin_ldap.html")
login := checkAdminLogin(w, r) user := checkAdminLogin(w, r)
if login == nil { if user == nil {
return return
} }
@ -445,7 +445,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
modify_request := ldap.NewModifyRequest(dn, nil) modify_request := ldap.NewModifyRequest(dn, nil)
modify_request.Replace(attr, values_filtered) modify_request.Replace(attr, values_filtered)
err := login.conn.Modify(modify_request) err := user.Login.conn.Modify(modify_request)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
@ -466,7 +466,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
modify_request := ldap.NewModifyRequest(dn, nil) modify_request := ldap.NewModifyRequest(dn, nil)
modify_request.Add(attr, values_filtered) modify_request.Add(attr, values_filtered)
err := login.conn.Modify(modify_request) err := user.Login.conn.Modify(modify_request)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
@ -478,7 +478,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
modify_request := ldap.NewModifyRequest(dn, nil) modify_request := ldap.NewModifyRequest(dn, nil)
modify_request.Replace(attr, []string{}) modify_request.Replace(attr, []string{})
err := login.conn.Modify(modify_request) err := user.Login.conn.Modify(modify_request)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
@ -489,7 +489,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
modify_request := ldap.NewModifyRequest(group, nil) modify_request := ldap.NewModifyRequest(group, nil)
modify_request.Delete("member", []string{dn}) modify_request.Delete("member", []string{dn})
err := login.conn.Modify(modify_request) err := user.Login.conn.Modify(modify_request)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
@ -500,7 +500,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
modify_request := ldap.NewModifyRequest(group, nil) modify_request := ldap.NewModifyRequest(group, nil)
modify_request.Add("member", []string{dn}) modify_request.Add("member", []string{dn})
err := login.conn.Modify(modify_request) err := user.Login.conn.Modify(modify_request)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
@ -511,7 +511,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
modify_request := ldap.NewModifyRequest(dn, nil) modify_request := ldap.NewModifyRequest(dn, nil)
modify_request.Delete("member", []string{member}) modify_request.Delete("member", []string{member})
err := login.conn.Modify(modify_request) err := user.Login.conn.Modify(modify_request)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
@ -519,7 +519,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
} }
} else if action == "delete-object" { } else if action == "delete-object" {
del_request := ldap.NewDelRequest(dn, nil) del_request := ldap.NewDelRequest(dn, nil)
err := login.conn.Del(del_request) err := user.Login.conn.Del(del_request)
if err != nil { if err != nil {
dError = err.Error() dError = err.Error()
} else { } else {
@ -537,7 +537,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
[]string{}, []string{},
nil) nil)
sr, err := login.conn.Search(searchRequest) sr, err := user.Login.conn.Search(searchRequest)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -621,7 +621,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
fmt.Sprintf("(objectClass=organizationalPerson)"), fmt.Sprintf("(objectClass=organizationalPerson)"),
[]string{"dn", "displayname", "description"}, []string{"dn", "displayname", "description"},
nil) nil)
sr, err = login.conn.Search(searchRequest) sr, err = user.Login.conn.Search(searchRequest)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -675,7 +675,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
fmt.Sprintf("(objectClass=groupOfNames)"), fmt.Sprintf("(objectClass=groupOfNames)"),
[]string{"dn", "description"}, []string{"dn", "description"},
nil) nil)
sr, err = login.conn.Search(searchRequest) sr, err = user.Login.conn.Search(searchRequest)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -719,7 +719,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
[]string{"dn", "displayname", "description"}, []string{"dn", "displayname", "description"},
nil) nil)
sr, err = login.conn.Search(searchRequest) sr, err = user.Login.conn.Search(searchRequest)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -787,8 +787,8 @@ type CreateData struct {
func handleAdminCreate(w http.ResponseWriter, r *http.Request) { func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
templateAdminCreate := getTemplate("admin_create.html") templateAdminCreate := getTemplate("admin_create.html")
login := checkAdminLogin(w, r) user := checkAdminLogin(w, r)
if login == nil { if user == nil {
return return
} }
@ -803,7 +803,7 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
[]string{}, []string{},
nil) nil)
sr, err := login.conn.Search(searchRequest) sr, err := user.Login.conn.Search(searchRequest)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -894,7 +894,7 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
req.Attribute("description", []string{data.Description}) req.Attribute("description", []string{data.Description})
} }
err := login.conn.Add(req) err := user.Login.conn.Add(req)
if err != nil { if err != nil {
data.Error = err.Error() data.Error = err.Error()
} else { } else {

117
api.go
View file

@ -2,115 +2,14 @@ package main
import ( import (
//"context" //"context"
"encoding/json"
"errors" "errors"
"fmt" "encoding/json"
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
"github.com/go-ldap/ldap/v3"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"log" "log"
"net/http" "net/http"
"strings"
) )
func checkLoginAPI(w http.ResponseWriter, r *http.Request) (*LoginStatus, error) {
username, password, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return nil, errors.New("Missing or invalid 'Authenticate: Basic' field")
}
user_dn := buildUserDN(username)
login_info := &LoginInfo{
DN: user_dn,
Username: username,
Password: password,
}
l := ldapOpen(w)
if l == nil {
log.Println("Unable to open LDAP connection")
return nil, errors.New("Unable to open LDAP connection")
}
err := l.Bind(login_info.DN, login_info.Password)
if err != nil {
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return nil, errors.New("Unable to bind this user+password combination on the LDAP server")
}
loginStatus := &LoginStatus{
Info: login_info,
conn: l,
}
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",
"memberof",
"description",
"garage_s3_access_key",
FIELD_NAME_DIRECTORY_VISIBILITY,
FIELD_NAME_PROFILE_PICTURE,
},
nil)
sr, err := l.Search(searchRequest)
if err != nil {
log.Println(err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return nil, errors.New("Unable to search essential information about the logged user on LDAP")
}
if len(sr.Entries) != 1 {
log.Println(fmt.Sprintf("Unable to find entry for %s", login_info.DN))
http.Error(w, "Internal server error", http.StatusInternalServerError)
return nil, errors.New("Not enough or too many entries for this user in the LDAP directory (expect a unique result)")
}
loginStatus.UserEntry = sr.Entries[0]
loginStatus.CanAdmin = strings.EqualFold(loginStatus.Info.DN, config.AdminAccount)
loginStatus.CanInvite = false
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, nil
}
func checkLoginAndS3API(w http.ResponseWriter, r *http.Request) (*LoginStatus, *garage.KeyInfo, error) {
login, err := checkLoginAPI(w, r)
if err != nil {
return nil, nil, err
}
keyPair, err := checkS3(login)
return login, keyPair, err
}
type ApiQuotaView struct { type ApiQuotaView struct {
files *uint64 files *uint64
size *uint64 size *uint64
@ -131,6 +30,7 @@ type BucketRequest struct {
} }
func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) { func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) {
br, err := buildBucketRequest(w, r) br, err := buildBucketRequest(w, r)
if err != nil { if err != nil {
return return
@ -151,10 +51,9 @@ func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) {
} }
func buildBucketRequest(w http.ResponseWriter, r *http.Request) (*BucketRequest, error) { func buildBucketRequest(w http.ResponseWriter, r *http.Request) (*BucketRequest, error) {
_, s3key, err := checkLoginAndS3API(w, r) user := RequireUserApi(w, r)
if err != nil { if user == nil {
//http.Error(w, "Unable to connect on LDAP", http.StatusUnauthorized) return nil, errors.New("Unable to fetch user")
return nil, err
} }
// FETCH BUCKET ID by iterating over buckets owned by this key // FETCH BUCKET ID by iterating over buckets owned by this key
@ -162,6 +61,11 @@ func buildBucketRequest(w http.ResponseWriter, r *http.Request) (*BucketRequest,
var bucketId *string var bucketId *string
var global *bool var global *bool
s3key, err := user.S3KeyInfo()
if err != nil {
return nil, err
}
findBucketIdLoop: findBucketIdLoop:
for _, bucket := range s3key.Buckets { for _, bucket := range s3key.Buckets {
for _, localAlias := range bucket.LocalAliases { for _, localAlias := range bucket.LocalAliases {
@ -192,6 +96,7 @@ findBucketIdLoop:
global: *global, global: *global,
http: r, http: r,
}, nil }, nil
} }
func patchGarageBucket(w http.ResponseWriter, br *BucketRequest) { func patchGarageBucket(w http.ResponseWriter, br *BucketRequest) {

View file

@ -15,8 +15,8 @@ const FIELD_NAME_DIRECTORY_VISIBILITY = "directoryVisibility"
func handleDirectory(w http.ResponseWriter, r *http.Request) { func handleDirectory(w http.ResponseWriter, r *http.Request) {
templateDirectory := getTemplate("directory.html") templateDirectory := getTemplate("directory.html")
login := checkLogin(w, r) user := RequireUserHtml(w, r)
if login == nil { if user == nil {
return return
} }
@ -49,8 +49,8 @@ func handleDirectorySearch(w http.ResponseWriter, r *http.Request) {
} }
//Log to allow the research //Log to allow the research
login := checkLogin(w, r) user := RequireUserHtml(w, r)
if login == nil { if user == nil {
http.Error(w, "Login required", http.StatusUnauthorized) http.Error(w, "Login required", http.StatusUnauthorized)
return return
} }
@ -69,7 +69,7 @@ func handleDirectorySearch(w http.ResponseWriter, r *http.Request) {
}, },
nil) nil)
sr, err := login.conn.Search(searchRequest) sr, err := user.Login.conn.Search(searchRequest)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return

100
garage.go
View file

@ -2,16 +2,15 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
"github.com/go-ldap/ldap/v3"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"log" "log"
"net/http" "net/http"
"strings" "strings"
) )
func gadmin() (*garage.APIClient, context.Context) { func gadmin() (*garage.APIClient, context.Context) {
// Set Host and other parameters // Set Host and other parameters
configuration := garage.NewConfiguration() configuration := garage.NewConfiguration()
@ -48,7 +47,9 @@ func grgGetKey(accessKey string) (*garage.KeyInfo, error) {
return resp, nil return resp, nil
} }
func grgCreateWebsite(gkey, bucket string) (*garage.BucketInfo, error) {
func grgCreateWebsite(gkey, bucket string, quotas *UserQuota) (*garage.BucketInfo, error) {
client, ctx := gadmin() client, ctx := gadmin()
br := garage.NewCreateBucketRequest() br := garage.NewCreateBucketRequest()
@ -79,9 +80,7 @@ func grgCreateWebsite(gkey, bucket string) (*garage.BucketInfo, error) {
wr.SetIndexDocument("index.html") wr.SetIndexDocument("index.html")
wr.SetErrorDocument("error.html") wr.SetErrorDocument("error.html")
qr := garage.NewUpdateBucketRequestQuotas() qr := quotas.DefaultWebsiteQuota()
qr.SetMaxSize(1024 * 1024 * 50) // 50MB
qr.SetMaxObjects(10000) //10k objects
ur := garage.NewUpdateBucketRequest() ur := garage.NewUpdateBucketRequest()
ur.SetWebsiteAccess(*wr) ur.SetWebsiteAccess(*wr)
@ -153,85 +152,37 @@ func grgGetBucket(bid string) (*garage.BucketInfo, error) {
} }
func checkS3(login *LoginStatus) (*garage.KeyInfo, error) { // --- Start page rendering functions
if login == nil {
return nil, errors.New("Login can't be nil")
}
keyID := login.UserEntry.GetAttributeValue("garage_s3_access_key")
if keyID == "" {
keyPair, err := grgCreateKey(login.Info.Username)
if err != nil {
return 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 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 keyPair, err
}
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")
}
keyPair, err := checkS3(login)
return login, keyPair, err
}
type keyView struct {
Status *LoginStatus
Key *garage.KeyInfo
}
func handleGarageKey(w http.ResponseWriter, r *http.Request) { func handleGarageKey(w http.ResponseWriter, r *http.Request) {
login, s3key, err := checkLoginAndS3(w, r) user := RequireUserHtml(w, r)
if err != nil { if user == nil {
log.Println(err)
return return
} }
view := keyView{Status: login, Key: s3key}
tKey := getTemplate("garage_key.html") tKey := getTemplate("garage_key.html")
tKey.Execute(w, &view) tKey.Execute(w, user)
}
type webListView struct {
Status *LoginStatus
Key *garage.KeyInfo
} }
func handleGarageWebsiteList(w http.ResponseWriter, r *http.Request) { func handleGarageWebsiteList(w http.ResponseWriter, r *http.Request) {
login, s3key, err := checkLoginAndS3(w, r) user := RequireUserHtml(w, r)
if err != nil { if user == nil {
log.Println(err)
return return
} }
view := webListView{Status: login, Key: s3key}
tWebsiteList := getTemplate("garage_website_list.html") tWebsiteList := getTemplate("garage_website_list.html")
tWebsiteList.Execute(w, &view) tWebsiteList.Execute(w, user)
} }
func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) { func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) {
_, s3key, err := checkLoginAndS3(w, r) user := RequireUserHtml(w, r)
if err != nil { if user == nil {
log.Println(err)
return return
} }
tWebsiteNew := getTemplate("garage_website_new.html") tWebsiteNew := getTemplate("garage_website_new.html")
if r.Method == "POST" { if r.Method == "POST" {
r.ParseForm() r.ParseForm()
log.Println(r.Form)
bucket := strings.Join(r.Form["bucket"], "") bucket := strings.Join(r.Form["bucket"], "")
if bucket == "" { if bucket == "" {
@ -244,7 +195,15 @@ func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) {
return return
} }
binfo, err := grgCreateWebsite(*s3key.AccessKeyId, bucket) keyInfo, err := user.S3KeyInfo()
if err != nil {
log.Println(err)
// @FIXME we need to return the error to the user
tWebsiteNew.Execute(w, nil)
return
}
binfo, err := grgCreateWebsite(*keyInfo.AccessKeyId, bucket, user.Quota)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
// @FIXME we need to return the error to the user // @FIXME we need to return the error to the user
@ -260,8 +219,7 @@ func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) {
} }
type webInspectView struct { type webInspectView struct {
Status *LoginStatus User *LoggedUser
Key *garage.KeyInfo
Bucket *garage.BucketInfo Bucket *garage.BucketInfo
IndexDoc string IndexDoc string
ErrorDoc string ErrorDoc string
@ -271,13 +229,14 @@ type webInspectView struct {
} }
func handleGarageWebsiteInspect(w http.ResponseWriter, r *http.Request) { func handleGarageWebsiteInspect(w http.ResponseWriter, r *http.Request) {
login, s3key, err := checkLoginAndS3(w, r) user := RequireUserHtml(w, r)
if err != nil { if user == nil {
log.Println(err)
return return
} }
bucketId := mux.Vars(r)["bucket"] bucketId := mux.Vars(r)["bucket"]
// @FIXME check that user owns the bucket....
binfo, err := grgGetBucket(bucketId) binfo, err := grgGetBucket(bucketId)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -288,8 +247,7 @@ func handleGarageWebsiteInspect(w http.ResponseWriter, r *http.Request) {
q := binfo.GetQuotas() q := binfo.GetQuotas()
view := webInspectView{ view := webInspectView{
Status: login, User: user,
Key: s3key,
Bucket: binfo, Bucket: binfo,
IndexDoc: (&wc).GetIndexDocument(), IndexDoc: (&wc).GetIndexDocument(),
ErrorDoc: (&wc).GetErrorDocument(), ErrorDoc: (&wc).GetErrorDocument(),

View file

@ -1,19 +1,19 @@
version: '3' version: '3'
services: services:
consul: consul:
image: consul image: hashicorp/consul:1.16
restart: "always" restart: "always"
expose: expose:
- 8500 - 8500
bottin: bottin:
image: dxflrs/bottin:dnp41vp8w24h4mbh0xg1mybzr1f46k41 image: dxflrs/bottin:7h18i30cckckaahv87d3c86pn4a7q41z
command: "-config /etc/bottin.json" #command: "-config /etc/bottin.json"
restart: "always" restart: "always"
depends_on: ["consul"] depends_on: ["consul"]
ports: ports:
- "389:389" - "389:389"
volumes: volumes:
- "./config/bottin.json:/etc/bottin.json" - "./config/bottin.json:/config.json"
garage: garage:
image: dxflrs/garage:v0.8.2 image: dxflrs/garage:v0.8.2
ports: ports:

View file

@ -21,29 +21,29 @@ import (
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])?)*$") 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 { func checkInviterLogin(w http.ResponseWriter, r *http.Request) *LoggedUser {
login := checkLogin(w, r) user := RequireUserHtml(w, r)
if login == nil { if user == nil {
return nil return nil
} }
if !login.CanInvite { if !user.Capabilities.CanInvite {
http.Error(w, "Not authorized to invite new users.", http.StatusUnauthorized) http.Error(w, "Not authorized to invite new users.", http.StatusUnauthorized)
return nil return nil
} }
return login return user
} }
// New account creation directly from interface // New account creation directly from interface
func handleInviteNewAccount(w http.ResponseWriter, r *http.Request) { func handleInviteNewAccount(w http.ResponseWriter, r *http.Request) {
login := checkInviterLogin(w, r) user := checkInviterLogin(w, r)
if login == nil { if user == nil {
return return
} }
handleNewAccount(w, r, login.conn, login.Info.DN) handleNewAccount(w, r, user.Login.conn, user.Login.Info.DN())
} }
// New account creation using code // New account creation using code
@ -52,13 +52,13 @@ func handleInvitationCode(w http.ResponseWriter, r *http.Request) {
code := mux.Vars(r)["code"] code := mux.Vars(r)["code"]
code_id, code_pw := readCode(code) code_id, code_pw := readCode(code)
l := ldapOpen(w) l, err := NewLdapCon()
if l == nil { if err != nil {
return http.Error(w, err.Error(), http.StatusInternalServerError)
} }
inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN
err := l.Bind(inviteDn, code_pw) err = l.Bind(inviteDn, code_pw)
if err != nil { if err != nil {
templateInviteInvalidCode := getTemplate("invite_invalid_code.html") templateInviteInvalidCode := getTemplate("invite_invalid_code.html")
templateInviteInvalidCode.Execute(w, nil) templateInviteInvalidCode.Execute(w, nil)
@ -241,8 +241,8 @@ type CodeMailFields struct {
func handleInviteSendCode(w http.ResponseWriter, r *http.Request) { func handleInviteSendCode(w http.ResponseWriter, r *http.Request) {
templateInviteSendCode := getTemplate("invite_send_code.html") templateInviteSendCode := getTemplate("invite_send_code.html")
login := checkInviterLogin(w, r) user := checkInviterLogin(w, r)
if login == nil { if user == nil {
return return
} }
@ -257,14 +257,14 @@ func handleInviteSendCode(w http.ResponseWriter, r *http.Request) {
sendto := strings.Join(r.Form["sendto"], "") sendto := strings.Join(r.Form["sendto"], "")
if choice == "display" || choice == "send" { if choice == "display" || choice == "send" {
trySendCode(login, choice, sendto, data) trySendCode(user, choice, sendto, data)
} }
} }
templateInviteSendCode.Execute(w, data) templateInviteSendCode.Execute(w, data)
} }
func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCodeData) { func trySendCode(user *LoggedUser, choice string, sendto string, data *SendCodeData) {
// Generate code // Generate code
code, code_id, code_pw := genCode() code, code_id, code_pw := genCode()
@ -279,7 +279,7 @@ func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCod
req.Attribute("userpassword", []string{pw}) req.Attribute("userpassword", []string{pw})
req.Attribute("objectclass", []string{"top", "invitationCode"}) req.Attribute("objectclass", []string{"top", "invitationCode"})
err = login.conn.Add(req) err = user.Login.conn.Add(req)
if err != nil { if err != nil {
data.ErrorMessage = err.Error() data.ErrorMessage = err.Error()
return return
@ -303,7 +303,7 @@ func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCod
templateMail.Execute(buf, &CodeMailFields{ templateMail.Execute(buf, &CodeMailFields{
To: sendto, To: sendto,
From: config.MailFrom, From: config.MailFrom,
InviteFrom: login.WelcomeName(), InviteFrom: user.WelcomeName(),
Code: code, Code: code,
WebBaseAddress: config.WebAddress, WebBaseAddress: config.WebAddress,
}) })

293
login.go Normal file
View file

@ -0,0 +1,293 @@
package main
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"strings"
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
"github.com/go-ldap/ldap/v3"
)
var (
ErrNotAuthenticatedSession = fmt.Errorf("User has no session")
ErrNotAuthenticatedBasic = fmt.Errorf("User has not sent Authentication Basic information")
ErrNotAuthenticated = fmt.Errorf("User is not authenticated")
ErrWrongLDAPCredentials = fmt.Errorf("LDAP credentials are wrong")
ErrLDAPServerUnreachable = fmt.Errorf("Unable to open the LDAP server")
ErrLDAPSearchInternalError = fmt.Errorf("LDAP Search of this user failed with an internal error")
ErrLDAPSearchNotFound = fmt.Errorf("User is authenticated but its associated data can not be found during search")
)
// --- Login Info ---
type LoginInfo struct {
Username string
Password string
}
func NewLoginInfoFromSession(r *http.Request) (*LoginInfo, error) {
session, err := store.Get(r, SESSION_NAME)
if err == nil {
username, ok_user := session.Values["login_username"]
password, ok_pwd := session.Values["login_password"]
if ok_user && ok_pwd {
loginInfo := &LoginInfo{
Username: username.(string),
Password: password.(string),
}
return loginInfo, nil
}
}
return nil, errors.Join(ErrNotAuthenticatedSession, err)
}
func NewLoginInfoFromBasicAuth(r *http.Request) (*LoginInfo, error) {
username, password, ok := r.BasicAuth()
if ok {
login_info := &LoginInfo{
Username: username,
Password: password,
}
return login_info, nil
}
return nil, ErrNotAuthenticatedBasic
}
func (li *LoginInfo) DN() string {
user_dn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, li.Username, config.UserBaseDN)
if strings.EqualFold(li.Username, config.AdminAccount) {
user_dn = li.Username
}
return user_dn
}
// --- Login Status ---
type LoginStatus struct {
Info *LoginInfo
conn *ldap.Conn
}
func NewLoginStatus(r *http.Request, login_info *LoginInfo) (*LoginStatus, error) {
l, err := NewLdapCon()
if err != nil {
return nil, err
}
err = l.Bind(login_info.DN(), login_info.Password)
if err != nil {
return nil, errors.Join(ErrWrongLDAPCredentials, err)
}
loginStatus := &LoginStatus{
Info: login_info,
conn: l,
}
return loginStatus, nil
}
func NewLdapCon() (*ldap.Conn, error) {
l, err := ldap.DialURL(config.LdapServerAddr)
if err != nil {
return nil, errors.Join(ErrLDAPServerUnreachable, err)
}
if config.LdapTLS {
err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
return nil, errors.Join(ErrLDAPServerUnreachable, err)
}
}
return l, nil
}
// --- Capabilities ---
type Capabilities struct {
CanAdmin bool
CanInvite bool
}
func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities {
// Initialize
canAdmin := false
canInvite := false
// Special case for the "admin" account that is de-facto admin
canAdmin = strings.EqualFold(login.Info.DN(), config.AdminAccount)
// Check if this account is part of a group that give capabilities
for _, attr := range entry.Attributes {
if strings.EqualFold(attr.Name, "memberof") {
for _, group := range attr.Values {
if config.GroupCanInvite != "" && strings.EqualFold(group, config.GroupCanInvite) {
canInvite = true
}
if config.GroupCanAdmin != "" && strings.EqualFold(group, config.GroupCanAdmin) {
canAdmin = true
}
}
}
}
return &Capabilities{
CanAdmin: canAdmin,
CanInvite: canInvite,
}
}
// --- Logged User ---
type LoggedUser struct {
Login *LoginStatus
Entry *ldap.Entry
Capabilities *Capabilities
Quota *UserQuota
s3key *garage.KeyInfo
}
func NewLoggedUser(login *LoginStatus) (*LoggedUser, error) {
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",
"memberof",
"description",
"garage_s3_access_key",
FIELD_NAME_DIRECTORY_VISIBILITY,
FIELD_NAME_PROFILE_PICTURE,
FIELD_QUOTA_WEBSITE_SIZE_BURSTED,
FIELD_QUOTA_WEBSITE_COUNT,
},
nil)
sr, err := login.conn.Search(searchRequest)
if err != nil {
return nil, ErrLDAPSearchInternalError
}
if len(sr.Entries) != 1 {
return nil, ErrLDAPSearchNotFound
}
entry := sr.Entries[0]
lu := &LoggedUser {
Login: login,
Entry: entry,
Capabilities: NewCapabilities(login, entry),
Quota: NewUserQuotaFromEntry(entry),
}
return lu, nil
}
func (lu *LoggedUser) WelcomeName() string {
ret := lu.Entry.GetAttributeValue("givenname")
if ret == "" {
ret = lu.Entry.GetAttributeValue("displayname")
}
if ret == "" {
ret = lu.Login.Info.Username
}
return ret
}
func (lu *LoggedUser) S3KeyInfo() (*garage.KeyInfo, error) {
var err error
var keyPair *garage.KeyInfo
if lu.s3key == nil {
keyID := lu.Entry.GetAttributeValue("garage_s3_access_key")
if keyID == "" {
// If there is no S3Key in LDAP, generate it...
keyPair, err = grgCreateKey(lu.Login.Info.Username)
if err != nil {
return nil, err
}
modify_request := ldap.NewModifyRequest(lu.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 = lu.Login.conn.Modify(modify_request)
if err != nil {
return nil, err
}
} else {
// There is an S3 key in LDAP, fetch its descriptor...
keyPair, err = grgGetKey(keyID)
if err != nil {
return nil, err
}
}
// Cache the keypair...
lu.s3key = keyPair
}
return lu.s3key, nil
}
// --- Require User Check
func RequireUser(r *http.Request) (*LoggedUser, error) {
var login_info *LoginInfo
if li, err := NewLoginInfoFromSession(r); err == nil {
login_info = li
} else if li, err := NewLoginInfoFromBasicAuth(r); err == nil {
login_info = li
} else {
return nil, ErrNotAuthenticated
}
loginStatus, err := NewLoginStatus(r, login_info)
if err != nil {
return nil, err
}
return NewLoggedUser(loginStatus)
}
func RequireUserHtml(w http.ResponseWriter, r *http.Request) *LoggedUser {
user, err := RequireUser(r)
if errors.Is(err, ErrNotAuthenticated) || errors.Is(err, ErrWrongLDAPCredentials) {
http.Redirect(w, r, "/login", http.StatusFound)
return nil
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil
}
return user
}
func RequireUserApi(w http.ResponseWriter, r *http.Request) *LoggedUser {
user, err := RequireUser(r)
if errors.Is(err, ErrNotAuthenticated) || errors.Is(err, ErrWrongLDAPCredentials) {
http.Error(w, err.Error(), http.StatusUnauthorized)
return nil
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil
}
return user
}

198
main.go
View file

@ -2,10 +2,8 @@ package main
import ( import (
"crypto/rand" "crypto/rand"
"crypto/tls"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt"
"html/template" "html/template"
"io/ioutil" "io/ioutil"
"log" "log"
@ -146,6 +144,7 @@ func server(args []string) {
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/", handleHome) r.HandleFunc("/", handleHome)
r.HandleFunc("/login", handleLogin)
r.HandleFunc("/logout", handleLogout) r.HandleFunc("/logout", handleLogout)
r.HandleFunc("/api/unstable/garage/bucket/{bucket}", handleAPIGarageBucket) r.HandleFunc("/api/unstable/garage/bucket/{bucket}", handleAPIGarageBucket)
@ -183,31 +182,6 @@ func server(args []string) {
} }
} }
type LoginInfo struct {
Username string
DN string
Password string
}
type LoginStatus struct {
Info *LoginInfo
conn *ldap.Conn
UserEntry *ldap.Entry
CanAdmin bool
CanInvite bool
}
func (login *LoginStatus) WelcomeName() string {
ret := login.UserEntry.GetAttributeValue("givenname")
if ret == "" {
ret = login.UserEntry.GetAttributeValue("displayname")
}
if ret == "" {
ret = login.Info.Username
}
return ret
}
func logRequest(handler http.Handler) http.Handler { func logRequest(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL)
@ -215,149 +189,32 @@ func logRequest(handler http.Handler) http.Handler {
}) })
} }
func checkLogin(w http.ResponseWriter, r *http.Request) *LoginStatus {
var login_info *LoginInfo
session, err := store.Get(r, SESSION_NAME)
if err == 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),
}
}
}
if login_info == nil {
login_info = handleLogin(w, r)
if login_info == nil {
return nil
}
}
l := ldapOpen(w)
if l == nil {
return nil
}
err = l.Bind(login_info.DN, login_info.Password)
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)
}
loginStatus := &LoginStatus{
Info: login_info,
conn: l,
}
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",
"memberof",
"description",
"garage_s3_access_key",
FIELD_NAME_DIRECTORY_VISIBILITY,
FIELD_NAME_PROFILE_PICTURE,
},
nil)
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
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 ldapOpen(w http.ResponseWriter) *ldap.Conn {
l, err := ldap.DialURL(config.LdapServerAddr)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil
}
if config.LdapTLS {
err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil
}
}
return l
}
// Page handlers ---- // Page handlers ----
// --- Home Controller
type HomePageData struct { type HomePageData struct {
Login *LoginStatus User *LoggedUser
BaseDN string BaseDN string
} }
func handleHome(w http.ResponseWriter, r *http.Request) { func handleHome(w http.ResponseWriter, r *http.Request) {
templateHome := getTemplate("home.html") templateHome := getTemplate("home.html")
login := checkLogin(w, r) user := RequireUserHtml(w, r)
if login == nil { if user == nil {
return return
} }
data := &HomePageData{ data := &HomePageData{
Login: login, User: user,
BaseDN: config.BaseDN, BaseDN: config.BaseDN,
} }
templateHome.Execute(w, data) templateHome.Execute(w, data)
} }
// --- Logout Controller
func handleLogout(w http.ResponseWriter, r *http.Request) { func handleLogout(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, SESSION_NAME) session, err := store.Get(r, SESSION_NAME)
if err != nil { if err != nil {
@ -374,9 +231,10 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
return return
} }
http.Redirect(w, r, "/", http.StatusFound) http.Redirect(w, r, "/login", http.StatusFound)
} }
// --- Login Controller ---
type LoginFormData struct { type LoginFormData struct {
Username string Username string
WrongUser bool WrongUser bool
@ -384,34 +242,26 @@ type LoginFormData struct {
ErrorMessage string ErrorMessage string
} }
func buildUserDN(username string) string { func handleLogin(w http.ResponseWriter, r *http.Request) {
user_dn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, username, config.UserBaseDN)
if strings.EqualFold(username, config.AdminAccount) {
user_dn = username
}
return user_dn
}
func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo {
templateLogin := getTemplate("login.html") templateLogin := getTemplate("login.html")
if r.Method == "GET" { if r.Method == "GET" {
templateLogin.Execute(w, LoginFormData{}) templateLogin.Execute(w, LoginFormData{})
return nil return
} else if r.Method == "POST" { } else if r.Method == "POST" {
r.ParseForm() r.ParseForm()
username := strings.Join(r.Form["username"], "") username := strings.Join(r.Form["username"], "")
password := strings.Join(r.Form["password"], "") password := strings.Join(r.Form["password"], "")
user_dn := buildUserDN(username) loginInfo := LoginInfo { username, password }
l := ldapOpen(w) l, err := NewLdapCon()
if l == nil { if err != nil {
return nil http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
err := l.Bind(user_dn, password) err = l.Bind(loginInfo.DN(), loginInfo.Password)
if err != nil { if err != nil {
data := &LoginFormData{ data := &LoginFormData{
Username: username, Username: username,
@ -424,7 +274,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo {
data.ErrorMessage = err.Error() data.ErrorMessage = err.Error()
} }
templateLogin.Execute(w, data) templateLogin.Execute(w, data)
return nil return
} }
// Successfully logged in, save it to session // Successfully logged in, save it to session
@ -435,21 +285,15 @@ func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo {
session.Values["login_username"] = username session.Values["login_username"] = username
session.Values["login_password"] = password session.Values["login_password"] = password
session.Values["login_dn"] = user_dn
err = session.Save(r, w) err = session.Save(r, w)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return nil return
} }
return &LoginInfo{ http.Redirect(w, r, "/", http.StatusFound)
DN: user_dn,
Username: username,
Password: password,
}
} else { } else {
http.Error(w, "Unsupported method", http.StatusBadRequest) http.Error(w, "Unsupported method", http.StatusBadRequest)
return nil
} }
} }

View file

@ -44,7 +44,7 @@ func newMinioClient() (*minio.Client, error) {
} }
// Upload image through guichet server. // Upload image through guichet server.
func uploadProfilePicture(w http.ResponseWriter, r *http.Request, login *LoginStatus) (string, error) { func uploadProfilePicture(w http.ResponseWriter, r *http.Request, user *LoggedUser) (string, error) {
file, _, err := r.FormFile("image") file, _, err := r.FormFile("image")
if err == http.ErrMissingFile { if err == http.ErrMissingFile {
@ -74,7 +74,7 @@ func uploadProfilePicture(w http.ResponseWriter, r *http.Request, login *LoginSt
// If a previous profile picture existed, delete it // If a previous profile picture existed, delete it
// (don't care about errors) // (don't care about errors)
if nameConsul := login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE); nameConsul != "" { if nameConsul := user.Entry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE); nameConsul != "" {
mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul, minio.RemoveObjectOptions{}) mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul, minio.RemoveObjectOptions{})
mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul+"-thumb", minio.RemoveObjectOptions{}) mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul+"-thumb", minio.RemoveObjectOptions{})
} }
@ -144,9 +144,9 @@ func resizePicture(file multipart.File, buffFull, buffThumb *bytes.Buffer) error
func handleDownloadPicture(w http.ResponseWriter, r *http.Request) { func handleDownloadPicture(w http.ResponseWriter, r *http.Request) {
name := mux.Vars(r)["name"] name := mux.Vars(r)["name"]
//Check login // Get user
login := checkLogin(w, r) user := RequireUserHtml(w, r)
if login == nil { if user == nil {
return return
} }

View file

@ -8,7 +8,7 @@ import (
) )
type ProfileTplData struct { type ProfileTplData struct {
Status *LoginStatus User *LoggedUser
ErrorMessage string ErrorMessage string
Success bool Success bool
Mail string Mail string
@ -23,24 +23,24 @@ type ProfileTplData struct {
func handleProfile(w http.ResponseWriter, r *http.Request) { func handleProfile(w http.ResponseWriter, r *http.Request) {
templateProfile := getTemplate("profile.html") templateProfile := getTemplate("profile.html")
login := checkLogin(w, r) user := RequireUserHtml(w, r)
if login == nil { if user == nil {
return return
} }
data := &ProfileTplData{ data := &ProfileTplData{
Status: login, User: user,
ErrorMessage: "", ErrorMessage: "",
Success: false, Success: false,
} }
data.Mail = login.UserEntry.GetAttributeValue("mail") data.Mail = user.Entry.GetAttributeValue("mail")
data.DisplayName = login.UserEntry.GetAttributeValue("displayname") data.DisplayName = user.Entry.GetAttributeValue("displayname")
data.GivenName = login.UserEntry.GetAttributeValue("givenname") data.GivenName = user.Entry.GetAttributeValue("givenname")
data.Surname = login.UserEntry.GetAttributeValue("sn") data.Surname = user.Entry.GetAttributeValue("sn")
data.Visibility = login.UserEntry.GetAttributeValue(FIELD_NAME_DIRECTORY_VISIBILITY) data.Visibility = user.Entry.GetAttributeValue(FIELD_NAME_DIRECTORY_VISIBILITY)
data.Description = login.UserEntry.GetAttributeValue("description") data.Description = user.Entry.GetAttributeValue("description")
data.ProfilePicture = login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE) data.ProfilePicture = user.Entry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE)
if r.Method == "POST" { if r.Method == "POST" {
//5MB maximum size files //5MB maximum size files
@ -56,7 +56,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
} }
data.Visibility = visible data.Visibility = visible
profilePicture, err := uploadProfilePicture(w, r, login) profilePicture, err := uploadProfilePicture(w, r, user)
if err != nil { if err != nil {
data.ErrorMessage = err.Error() data.ErrorMessage = err.Error()
} }
@ -65,7 +65,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
data.ProfilePicture = profilePicture data.ProfilePicture = profilePicture
} }
modify_request := ldap.NewModifyRequest(login.Info.DN, nil) modify_request := ldap.NewModifyRequest(user.Login.Info.DN(), nil)
modify_request.Replace("displayname", []string{data.DisplayName}) modify_request.Replace("displayname", []string{data.DisplayName})
modify_request.Replace("givenname", []string{data.GivenName}) modify_request.Replace("givenname", []string{data.GivenName})
modify_request.Replace("sn", []string{data.Surname}) modify_request.Replace("sn", []string{data.Surname})
@ -75,7 +75,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
modify_request.Replace(FIELD_NAME_PROFILE_PICTURE, []string{data.ProfilePicture}) modify_request.Replace(FIELD_NAME_PROFILE_PICTURE, []string{data.ProfilePicture})
} }
err = login.conn.Modify(modify_request) err = user.Login.conn.Modify(modify_request)
if err != nil { if err != nil {
data.ErrorMessage = err.Error() data.ErrorMessage = err.Error()
} else { } else {
@ -88,7 +88,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
} }
type PasswdTplData struct { type PasswdTplData struct {
Status *LoginStatus User *LoggedUser
ErrorMessage string ErrorMessage string
TooShortError bool TooShortError bool
NoMatchError bool NoMatchError bool
@ -98,13 +98,13 @@ type PasswdTplData struct {
func handlePasswd(w http.ResponseWriter, r *http.Request) { func handlePasswd(w http.ResponseWriter, r *http.Request) {
templatePasswd := getTemplate("passwd.html") templatePasswd := getTemplate("passwd.html")
login := checkLogin(w, r) user := RequireUserHtml(w, r)
if login == nil { if user == nil {
return return
} }
data := &PasswdTplData{ data := &PasswdTplData{
Status: login, User: user,
ErrorMessage: "", ErrorMessage: "",
Success: false, Success: false,
} }
@ -120,11 +120,11 @@ func handlePasswd(w http.ResponseWriter, r *http.Request) {
} else if password2 != password { } else if password2 != password {
data.NoMatchError = true data.NoMatchError = true
} else { } else {
modify_request := ldap.NewModifyRequest(login.Info.DN, nil) modify_request := ldap.NewModifyRequest(user.Login.Info.DN(), nil)
pw, err := SSHAEncode(password) pw, err := SSHAEncode(password)
if err == nil { if err == nil {
modify_request.Replace("userpassword", []string{pw}) modify_request.Replace("userpassword", []string{pw})
err := login.conn.Modify(modify_request) err := user.Login.conn.Modify(modify_request)
if err != nil { if err != nil {
data.ErrorMessage = err.Error() data.ErrorMessage = err.Error()
} else { } else {

79
quotas.go Normal file
View file

@ -0,0 +1,79 @@
package main
import (
"errors"
"fmt"
"strconv"
"github.com/go-ldap/ldap/v3"
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
)
const (
// --- Default Quota Values ---
QUOTA_WEBSITE_SIZE_DEFAULT = 1024 * 1024 * 50 // 50MB
QUOTA_WEBSITE_SIZE_BURSTED = 1024 * 1024 * 200 // 200MB
QUOTA_WEBSITE_OBJECTS = 10000 // 10k objects
QUOTA_WEBSITE_COUNT = 5 // 5 buckets
// --- Per-user overridable fields ---
FIELD_QUOTA_WEBSITE_SIZE_BURSTED = "quota_website_size_bursted"
FIELD_QUOTA_WEBSITE_COUNT = "quota_website_count"
)
type UserQuota struct {
WebsiteCount int64
WebsiteSizeDefault int64
WebsiteSizeBursted int64
WebsiteObjects int64
}
func NewUserQuota() *UserQuota {
return &UserQuota {
WebsiteCount: QUOTA_WEBSITE_COUNT,
WebsiteSizeDefault: QUOTA_WEBSITE_SIZE_DEFAULT,
WebsiteSizeBursted: QUOTA_WEBSITE_SIZE_BURSTED,
WebsiteObjects: QUOTA_WEBSITE_OBJECTS,
}
}
var (
ErrQuotaEmpty = fmt.Errorf("No quota is defined for this entry")
ErrQuotaInvalid = fmt.Errorf("The defined quota can't be parsed")
)
func entryToQuota(entry *ldap.Entry, field string) (int64, error) {
f := entry.GetAttributeValue(field)
if f == "" {
return -1, ErrQuotaEmpty
}
q, err := strconv.ParseInt(f, 10, 64)
if err != nil {
return -1, errors.Join(ErrQuotaInvalid, err)
}
return q, nil
}
func NewUserQuotaFromEntry(entry *ldap.Entry) *UserQuota {
quotas := NewUserQuota()
if q, err := entryToQuota(entry, FIELD_QUOTA_WEBSITE_COUNT); err != nil {
quotas.WebsiteCount = q
}
if q, err := entryToQuota(entry, FIELD_QUOTA_WEBSITE_SIZE_BURSTED); err != nil {
quotas.WebsiteSizeBursted = q
}
return quotas
}
func (q *UserQuota) DefaultWebsiteQuota() *garage.UpdateBucketRequestQuotas {
qr := garage.NewUpdateBucketRequestQuotas()
qr.SetMaxSize(q.WebsiteSizeDefault)
qr.SetMaxObjects(q.WebsiteSizeBursted)
return qr
}