Merge pull request 'An API for Guichet' (#23) from api into main
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #23
This commit is contained in:
commit
49d8e81fbe
22 changed files with 1268 additions and 495 deletions
98
admin.go
98
admin.go
|
@ -11,18 +11,18 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func checkAdminLogin(w http.ResponseWriter, r *http.Request) *LoginStatus {
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
func checkAdminLogin(w http.ResponseWriter, r *http.Request) *LoggedUser {
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !login.CanAdmin {
|
||||
if !user.Capabilities.CanAdmin {
|
||||
http.Error(w, "Not authorized to perform administrative operations.", http.StatusUnauthorized)
|
||||
return nil
|
||||
}
|
||||
|
||||
return login
|
||||
return user
|
||||
}
|
||||
|
||||
type EntryList []*ldap.Entry
|
||||
|
@ -40,7 +40,7 @@ func (d EntryList) Less(i, j int) bool {
|
|||
}
|
||||
|
||||
type AdminUsersTplData struct {
|
||||
Login *LoginStatus
|
||||
User *LoggedUser
|
||||
UserNameAttr string
|
||||
UserBaseDN string
|
||||
Users EntryList
|
||||
|
@ -49,8 +49,8 @@ type AdminUsersTplData struct {
|
|||
func handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
||||
templateAdminUsers := getTemplate("admin_users.html")
|
||||
|
||||
login := checkAdminLogin(w, r)
|
||||
if login == nil {
|
||||
user := checkAdminLogin(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -61,14 +61,14 @@ func handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
|||
[]string{config.UserNameAttr, "dn", "displayname", "givenname", "sn", "mail"},
|
||||
nil)
|
||||
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := &AdminUsersTplData{
|
||||
Login: login,
|
||||
User: user,
|
||||
UserNameAttr: config.UserNameAttr,
|
||||
UserBaseDN: config.UserBaseDN,
|
||||
Users: EntryList(sr.Entries),
|
||||
|
@ -79,7 +79,7 @@ func handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
type AdminGroupsTplData struct {
|
||||
Login *LoginStatus
|
||||
User *LoggedUser
|
||||
GroupNameAttr string
|
||||
GroupBaseDN string
|
||||
Groups EntryList
|
||||
|
@ -88,8 +88,8 @@ type AdminGroupsTplData struct {
|
|||
func handleAdminGroups(w http.ResponseWriter, r *http.Request) {
|
||||
templateAdminGroups := getTemplate("admin_groups.html")
|
||||
|
||||
login := checkAdminLogin(w, r)
|
||||
if login == nil {
|
||||
user := checkAdminLogin(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -100,14 +100,14 @@ func handleAdminGroups(w http.ResponseWriter, r *http.Request) {
|
|||
[]string{config.GroupNameAttr, "dn", "description"},
|
||||
nil)
|
||||
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := &AdminGroupsTplData{
|
||||
Login: login,
|
||||
User: user,
|
||||
GroupNameAttr: config.GroupNameAttr,
|
||||
GroupBaseDN: config.GroupBaseDN,
|
||||
Groups: EntryList(sr.Entries),
|
||||
|
@ -118,7 +118,7 @@ func handleAdminGroups(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
type AdminMailingTplData struct {
|
||||
Login *LoginStatus
|
||||
User *LoggedUser
|
||||
MailingNameAttr string
|
||||
MailingBaseDN string
|
||||
MailingLists EntryList
|
||||
|
@ -127,8 +127,8 @@ type AdminMailingTplData struct {
|
|||
func handleAdminMailing(w http.ResponseWriter, r *http.Request) {
|
||||
templateAdminMailing := getTemplate("admin_mailing.html")
|
||||
|
||||
login := checkAdminLogin(w, r)
|
||||
if login == nil {
|
||||
user := checkAdminLogin(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -139,14 +139,14 @@ func handleAdminMailing(w http.ResponseWriter, r *http.Request) {
|
|||
[]string{config.MailingNameAttr, "dn", "description"},
|
||||
nil)
|
||||
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := &AdminMailingTplData{
|
||||
Login: login,
|
||||
User: user,
|
||||
MailingNameAttr: config.MailingNameAttr,
|
||||
MailingBaseDN: config.MailingBaseDN,
|
||||
MailingLists: EntryList(sr.Entries),
|
||||
|
@ -157,7 +157,7 @@ func handleAdminMailing(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
type AdminMailingListTplData struct {
|
||||
Login *LoginStatus
|
||||
User *LoggedUser
|
||||
MailingNameAttr string
|
||||
MailingBaseDN string
|
||||
|
||||
|
@ -173,8 +173,8 @@ type AdminMailingListTplData struct {
|
|||
func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
|
||||
templateAdminMailingList := getTemplate("admin_mailing_list.html")
|
||||
|
||||
login := checkAdminLogin(w, r)
|
||||
if login == nil {
|
||||
user := checkAdminLogin(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -193,7 +193,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Add("member", []string{member})
|
||||
|
||||
err := login.conn.Modify(modify_request)
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -209,7 +209,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
|
|||
fmt.Sprintf("(&(objectClass=organizationalPerson)(mail=%s))", mail),
|
||||
[]string{"dn", "displayname", "mail"},
|
||||
nil)
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -222,14 +222,14 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
|
|||
if displayname != "" {
|
||||
req.Attribute("displayname", []string{displayname})
|
||||
}
|
||||
err := login.conn.Add(req)
|
||||
err := user.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)
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -243,7 +243,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
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 {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -258,7 +258,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Delete("member", []string{member})
|
||||
|
||||
err := login.conn.Modify(modify_request)
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -275,7 +275,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
|
|||
[]string{"dn", config.MailingNameAttr, "member", "description"},
|
||||
nil)
|
||||
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -307,7 +307,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
|
|||
fmt.Sprintf("(objectClass=organizationalPerson)"),
|
||||
[]string{"dn", "displayname", "mail"},
|
||||
nil)
|
||||
sr, err = login.conn.Search(searchRequest)
|
||||
sr, err = user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -322,7 +322,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
data := &AdminMailingListTplData{
|
||||
Login: login,
|
||||
User: user,
|
||||
MailingNameAttr: config.MailingNameAttr,
|
||||
MailingBaseDN: config.MailingBaseDN,
|
||||
|
||||
|
@ -394,8 +394,8 @@ type PropValues struct {
|
|||
func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
||||
templateAdminLDAP := getTemplate("admin_ldap.html")
|
||||
|
||||
login := checkAdminLogin(w, r)
|
||||
if login == nil {
|
||||
user := checkAdminLogin(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -445,7 +445,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Replace(attr, values_filtered)
|
||||
|
||||
err := login.conn.Modify(modify_request)
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -466,7 +466,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Add(attr, values_filtered)
|
||||
|
||||
err := login.conn.Modify(modify_request)
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -478,7 +478,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Replace(attr, []string{})
|
||||
|
||||
err := login.conn.Modify(modify_request)
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -489,7 +489,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(group, nil)
|
||||
modify_request.Delete("member", []string{dn})
|
||||
|
||||
err := login.conn.Modify(modify_request)
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -500,7 +500,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(group, nil)
|
||||
modify_request.Add("member", []string{dn})
|
||||
|
||||
err := login.conn.Modify(modify_request)
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -511,7 +511,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Delete("member", []string{member})
|
||||
|
||||
err := login.conn.Modify(modify_request)
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -519,7 +519,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
} else if action == "delete-object" {
|
||||
del_request := ldap.NewDelRequest(dn, nil)
|
||||
err := login.conn.Del(del_request)
|
||||
err := user.Login.conn.Del(del_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -537,7 +537,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
[]string{},
|
||||
nil)
|
||||
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -621,7 +621,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
fmt.Sprintf("(objectClass=organizationalPerson)"),
|
||||
[]string{"dn", "displayname", "description"},
|
||||
nil)
|
||||
sr, err = login.conn.Search(searchRequest)
|
||||
sr, err = user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -675,7 +675,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
fmt.Sprintf("(objectClass=groupOfNames)"),
|
||||
[]string{"dn", "description"},
|
||||
nil)
|
||||
sr, err = login.conn.Search(searchRequest)
|
||||
sr, err = user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -719,7 +719,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
[]string{"dn", "displayname", "description"},
|
||||
nil)
|
||||
|
||||
sr, err = login.conn.Search(searchRequest)
|
||||
sr, err = user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -787,8 +787,8 @@ type CreateData struct {
|
|||
func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
|
||||
templateAdminCreate := getTemplate("admin_create.html")
|
||||
|
||||
login := checkAdminLogin(w, r)
|
||||
if login == nil {
|
||||
user := checkAdminLogin(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -803,7 +803,7 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
|
|||
[]string{},
|
||||
nil)
|
||||
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -894,7 +894,7 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
|
|||
req.Attribute("description", []string{data.Description})
|
||||
}
|
||||
|
||||
err := login.conn.Add(req)
|
||||
err := user.Login.conn.Add(req)
|
||||
if err != nil {
|
||||
data.Error = err.Error()
|
||||
} else {
|
||||
|
|
127
api.go
Normal file
127
api.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func handleAPIWebsiteList(w http.ResponseWriter, r *http.Request) {
|
||||
user := RequireUserApi(w, r)
|
||||
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctrl, err := NewWebsiteController(user)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
describe, err := ctrl.Describe()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(describe)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "This method is not implemented for this endpoint", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
func handleAPIWebsiteInspect(w http.ResponseWriter, r *http.Request) {
|
||||
user := RequireUserApi(w, r)
|
||||
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
bucketName := mux.Vars(r)["bucket"]
|
||||
ctrl, err := NewWebsiteController(user)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
view, err := ctrl.Inspect(bucketName)
|
||||
if errors.Is(err, ErrWebsiteNotFound) {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(view)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
view, err := ctrl.Create(bucketName)
|
||||
if errors.Is(err, ErrEmptyBucketName) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
} else if errors.Is(err, ErrWebsiteQuotaReached) {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(view)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPatch {
|
||||
var patch WebsitePatch
|
||||
err := json.NewDecoder(r.Body).Decode(&patch)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Join(fmt.Errorf("Can't parse the request body as a website patch JSON"), err).Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
view, err := ctrl.Patch(bucketName, &patch)
|
||||
if errors.Is(err, ErrWebsiteNotFound) {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(view)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodDelete {
|
||||
err := ctrl.Delete(bucketName)
|
||||
if errors.Is(err, ErrEmptyBucketName) || errors.Is(err, ErrBucketDeleteNotEmpty) || errors.Is(err, ErrBucketDeleteUnfinishedUpload) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
} else if errors.Is(err, ErrWebsiteNotFound) {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
http.Error(w, "This method is not implemented for this endpoint", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
44
cli.go
Normal file
44
cli.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"golang.org/x/term"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var fsCli = flag.NewFlagSet("cli", flag.ContinueOnError)
|
||||
var passFlag = fsCli.Bool("passwd", false, "Tool to generate a guichet-compatible password hash")
|
||||
|
||||
func cliMain(args []string) {
|
||||
if err := fsCli.Parse(args); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *passFlag {
|
||||
cliPasswd()
|
||||
} else {
|
||||
fsCli.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func cliPasswd() {
|
||||
fmt.Print("Password: ")
|
||||
bytepw, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
pass := string(bytepw)
|
||||
|
||||
hash, err := SSHAEncode(pass)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println(hash)
|
||||
}
|
10
directory.go
10
directory.go
|
@ -15,8 +15,8 @@ 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 {
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -49,8 +49,8 @@ func handleDirectorySearch(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
//Log to allow the research
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
http.Error(w, "Login required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ func handleDirectorySearch(w http.ResponseWriter, r *http.Request) {
|
|||
},
|
||||
nil)
|
||||
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
|
258
garage.go
258
garage.go
|
@ -2,10 +2,8 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/gorilla/mux"
|
||||
"log"
|
||||
"net/http"
|
||||
|
@ -48,7 +46,7 @@ func grgGetKey(accessKey string) (*garage.KeyInfo, error) {
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
func grgCreateWebsite(gkey, bucket string) (*garage.BucketInfo, error) {
|
||||
func grgCreateBucket(bucket string) (*garage.BucketInfo, error) {
|
||||
client, ctx := gadmin()
|
||||
|
||||
br := garage.NewCreateBucketRequest()
|
||||
|
@ -60,34 +58,40 @@ func grgCreateWebsite(gkey, bucket string) (*garage.BucketInfo, error) {
|
|||
fmt.Printf("%+v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
return binfo, nil
|
||||
}
|
||||
|
||||
func grgAllowKeyOnBucket(bid, gkey string) (*garage.BucketInfo, error) {
|
||||
client, ctx := gadmin()
|
||||
|
||||
// Allow user's key
|
||||
ar := garage.AllowBucketKeyRequest{
|
||||
BucketId: *binfo.Id,
|
||||
BucketId: bid,
|
||||
AccessKeyId: gkey,
|
||||
Permissions: *garage.NewAllowBucketKeyRequestPermissions(true, true, true),
|
||||
}
|
||||
binfo, _, err = client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute()
|
||||
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
|
||||
return binfo, nil
|
||||
}
|
||||
|
||||
func allowWebsiteDefault() *garage.UpdateBucketRequestWebsiteAccess {
|
||||
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
|
||||
return wr
|
||||
}
|
||||
|
||||
ur := garage.NewUpdateBucketRequest()
|
||||
ur.SetWebsiteAccess(*wr)
|
||||
ur.SetQuotas(*qr)
|
||||
func grgUpdateBucket(bid string, ur *garage.UpdateBucketRequest) (*garage.BucketInfo, error) {
|
||||
client, ctx := gadmin()
|
||||
|
||||
binfo, _, err = client.BucketApi.UpdateBucket(ctx, *binfo.Id).UpdateBucketRequest(*ur).Execute()
|
||||
binfo, _, err := client.BucketApi.UpdateBucket(ctx, bid).UpdateBucketRequest(*ur).Execute()
|
||||
if err != nil {
|
||||
fmt.Printf("%+v\n", err)
|
||||
return nil, err
|
||||
|
@ -97,155 +101,197 @@ func grgCreateWebsite(gkey, bucket string) (*garage.BucketInfo, error) {
|
|||
return binfo, nil
|
||||
}
|
||||
|
||||
func grgAddGlobalAlias(bid, alias string) (*garage.BucketInfo, error) {
|
||||
client, ctx := gadmin()
|
||||
|
||||
resp, _, err := client.BucketApi.PutBucketGlobalAlias(ctx).Id(bid).Alias(alias).Execute()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func grgAddLocalAlias(bid, key, alias string) (*garage.BucketInfo, error) {
|
||||
client, ctx := gadmin()
|
||||
|
||||
resp, _, err := client.BucketApi.PutBucketLocalAlias(ctx).Id(bid).AccessKeyId(key).Alias(alias).Execute()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func grgDelGlobalAlias(bid, alias string) (*garage.BucketInfo, error) {
|
||||
client, ctx := gadmin()
|
||||
|
||||
resp, _, err := client.BucketApi.DeleteBucketGlobalAlias(ctx).Id(bid).Alias(alias).Execute()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func grgDelLocalAlias(bid, key, alias string) (*garage.BucketInfo, error) {
|
||||
client, ctx := gadmin()
|
||||
|
||||
resp, _, err := client.BucketApi.DeleteBucketLocalAlias(ctx).Id(bid).AccessKeyId(key).Alias(alias).Execute()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
return resp, 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)
|
||||
log.Println(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")
|
||||
}
|
||||
func grgDeleteBucket(bid string) error {
|
||||
client, ctx := gadmin()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type keyView struct {
|
||||
Status *LoginStatus
|
||||
Key *garage.KeyInfo
|
||||
}
|
||||
|
||||
func handleGarageKey(w http.ResponseWriter, r *http.Request) {
|
||||
login, s3key, err := checkLoginAndS3(w, r)
|
||||
_, err := client.BucketApi.DeleteBucket(ctx, bid).Execute()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// --- Start page rendering functions
|
||||
|
||||
func handleWebsiteConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
view := keyView{Status: login, Key: s3key}
|
||||
|
||||
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) {
|
||||
login, s3key, err := checkLoginAndS3(w, r)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
func handleWebsiteList(w http.ResponseWriter, r *http.Request) {
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
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)
|
||||
ctrl, err := NewWebsiteController(user)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(ctrl.PrettyList) > 0 {
|
||||
http.Redirect(w, r, "/website/inspect/"+ctrl.PrettyList[0], http.StatusFound)
|
||||
} else {
|
||||
http.Redirect(w, r, "/website/new", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
type WebsiteNewTpl struct {
|
||||
Ctrl *WebsiteController
|
||||
Err error
|
||||
}
|
||||
|
||||
func handleWebsiteNew(w http.ResponseWriter, r *http.Request) {
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctrl, err := NewWebsiteController(user)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tpl := &WebsiteNewTpl{ctrl, nil}
|
||||
|
||||
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)
|
||||
view, err := ctrl.Create(bucket)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
// @FIXME we need to return the error to the user
|
||||
tWebsiteNew.Execute(w, nil)
|
||||
tpl.Err = err
|
||||
tWebsiteNew.Execute(w, tpl)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/garage/website/b/"+*binfo.Id, http.StatusFound)
|
||||
http.Redirect(w, r, "/website/inspect/"+view.Name.Pretty, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
tWebsiteNew.Execute(w, nil)
|
||||
tWebsiteNew.Execute(w, tpl)
|
||||
}
|
||||
|
||||
type webInspectView struct {
|
||||
Status *LoginStatus
|
||||
Key *garage.KeyInfo
|
||||
Bucket *garage.BucketInfo
|
||||
IndexDoc string
|
||||
ErrorDoc string
|
||||
MaxObjects int64
|
||||
MaxSize int64
|
||||
UsedSizePct float64
|
||||
type WebsiteInspectTpl struct {
|
||||
Describe *WebsiteDescribe
|
||||
View *WebsiteView
|
||||
Err error
|
||||
}
|
||||
|
||||
func handleGarageWebsiteInspect(w http.ResponseWriter, r *http.Request) {
|
||||
login, s3key, err := checkLoginAndS3(w, r)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) {
|
||||
var processErr error
|
||||
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
bucketId := mux.Vars(r)["bucket"]
|
||||
binfo, err := grgGetBucket(bucketId)
|
||||
ctrl, err := NewWebsiteController(user)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
wc := binfo.GetWebsiteConfig()
|
||||
q := binfo.GetQuotas()
|
||||
bucketName := mux.Vars(r)["bucket"]
|
||||
|
||||
if r.Method == "POST" {
|
||||
r.ParseForm()
|
||||
action := strings.Join(r.Form["action"], "")
|
||||
switch action {
|
||||
case "increase_quota":
|
||||
_, processErr = ctrl.Patch(bucketName, &WebsitePatch{Size: &user.Quota.WebsiteSizeBursted})
|
||||
case "delete_bucket":
|
||||
processErr = ctrl.Delete(bucketName)
|
||||
http.Redirect(w, r, "/website", http.StatusFound)
|
||||
return
|
||||
default:
|
||||
processErr = fmt.Errorf("Unknown action")
|
||||
}
|
||||
|
||||
view := webInspectView{
|
||||
Status: login,
|
||||
Key: s3key,
|
||||
Bucket: binfo,
|
||||
IndexDoc: (&wc).GetIndexDocument(),
|
||||
ErrorDoc: (&wc).GetErrorDocument(),
|
||||
MaxObjects: (&q).GetMaxObjects(),
|
||||
MaxSize: (&q).GetMaxSize(),
|
||||
}
|
||||
|
||||
view, err := ctrl.Inspect(bucketName)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
describe, err := ctrl.Describe()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tpl := &WebsiteInspectTpl{describe, view, processErr}
|
||||
|
||||
tWebsiteInspect := getTemplate("garage_website_inspect.html")
|
||||
tWebsiteInspect.Execute(w, &view)
|
||||
tWebsiteInspect.Execute(w, &tpl)
|
||||
}
|
||||
|
|
3
go.mod
3
go.mod
|
@ -14,6 +14,7 @@ require (
|
|||
github.com/minio/minio-go/v7 v7.0.0
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/term v0.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -29,7 +30,7 @@ require (
|
|||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
|
|
5
go.sum
5
go.sum
|
@ -278,8 +278,11 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
|
@ -6,8 +6,14 @@
|
|||
"ANONYMOUS::bind:*,ou=users,dc=bottin,dc=eu:",
|
||||
"ANONYMOUS::bind:cn=admin,dc=bottin,dc=eu:",
|
||||
"*,dc=bottin,dc=eu::read:*:* !userpassword",
|
||||
"*::read modify:SELF:*",
|
||||
"cn=admin,dc=bottin,dc=eu::read add modify delete:*:*",
|
||||
"*:cn=admin,ou=groups,dc=bottin,dc=eu:read add modify delete:*:*"
|
||||
"*:cn=admin,ou=groups,dc=bottin,dc=eu:read add modify delete:*:*",
|
||||
|
||||
"ANONYMOUS::bind:*,ou=invitations,dc=bottin,dc=eu:",
|
||||
"*,ou=invitations,dc=bottin,dc=eu::delete:SELF:*",
|
||||
"*,ou=invitations,dc=bottin,dc=eu::add:*,ou=users,dc=bottin,dc=eu:*",
|
||||
"*,ou=invitations,dc=bottin,dc=eu::modifyAdd:cn=email,ou=groups,dc=bottin,dc=eu:*",
|
||||
|
||||
"*::read modify:SELF:*"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
version: '3'
|
||||
services:
|
||||
consul:
|
||||
image: consul
|
||||
image: hashicorp/consul:1.16
|
||||
restart: "always"
|
||||
expose:
|
||||
- 8500
|
||||
bottin:
|
||||
image: dxflrs/bottin:dnp41vp8w24h4mbh0xg1mybzr1f46k41
|
||||
command: "-config /etc/bottin.json"
|
||||
image: dxflrs/bottin:7h18i30cckckaahv87d3c86pn4a7q41z
|
||||
#command: "-config /etc/bottin.json"
|
||||
restart: "always"
|
||||
depends_on: ["consul"]
|
||||
ports:
|
||||
- "389:389"
|
||||
volumes:
|
||||
- "./config/bottin.json:/etc/bottin.json"
|
||||
- "./config/bottin.json:/config.json"
|
||||
garage:
|
||||
image: dxflrs/garage:v0.8.2
|
||||
ports:
|
||||
|
|
37
invite.go
37
invite.go
|
@ -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])?)*$")
|
||||
|
||||
func checkInviterLogin(w http.ResponseWriter, r *http.Request) *LoginStatus {
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
func checkInviterLogin(w http.ResponseWriter, r *http.Request) *LoggedUser {
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !login.CanInvite {
|
||||
if !user.Capabilities.CanInvite {
|
||||
http.Error(w, "Not authorized to invite new users.", http.StatusUnauthorized)
|
||||
return nil
|
||||
}
|
||||
|
||||
return login
|
||||
return user
|
||||
}
|
||||
|
||||
// New account creation directly from interface
|
||||
|
||||
func handleInviteNewAccount(w http.ResponseWriter, r *http.Request) {
|
||||
login := checkInviterLogin(w, r)
|
||||
if login == nil {
|
||||
user := checkInviterLogin(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
handleNewAccount(w, r, login.conn, login.Info.DN)
|
||||
handleNewAccount(w, r, user.Login.conn, user.Login.Info.DN())
|
||||
}
|
||||
|
||||
// New account creation using code
|
||||
|
@ -52,14 +52,15 @@ func handleInvitationCode(w http.ResponseWriter, r *http.Request) {
|
|||
code := mux.Vars(r)["code"]
|
||||
code_id, code_pw := readCode(code)
|
||||
|
||||
l := ldapOpen(w)
|
||||
if l == nil {
|
||||
return
|
||||
l, err := NewLdapCon()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN
|
||||
err := l.Bind(inviteDn, code_pw)
|
||||
err = l.Bind(inviteDn, code_pw)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
templateInviteInvalidCode := getTemplate("invite_invalid_code.html")
|
||||
templateInviteInvalidCode.Execute(w, nil)
|
||||
return
|
||||
|
@ -241,8 +242,8 @@ type CodeMailFields struct {
|
|||
func handleInviteSendCode(w http.ResponseWriter, r *http.Request) {
|
||||
templateInviteSendCode := getTemplate("invite_send_code.html")
|
||||
|
||||
login := checkInviterLogin(w, r)
|
||||
if login == nil {
|
||||
user := checkInviterLogin(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -257,14 +258,14 @@ func handleInviteSendCode(w http.ResponseWriter, r *http.Request) {
|
|||
sendto := strings.Join(r.Form["sendto"], "")
|
||||
|
||||
if choice == "display" || choice == "send" {
|
||||
trySendCode(login, choice, sendto, data)
|
||||
trySendCode(user, choice, sendto, 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
|
||||
code, code_id, code_pw := genCode()
|
||||
|
||||
|
@ -279,7 +280,7 @@ func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCod
|
|||
req.Attribute("userpassword", []string{pw})
|
||||
req.Attribute("objectclass", []string{"top", "invitationCode"})
|
||||
|
||||
err = login.conn.Add(req)
|
||||
err = user.Login.conn.Add(req)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
return
|
||||
|
@ -303,7 +304,7 @@ func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCod
|
|||
templateMail.Execute(buf, &CodeMailFields{
|
||||
To: sendto,
|
||||
From: config.MailFrom,
|
||||
InviteFrom: login.WelcomeName(),
|
||||
InviteFrom: user.WelcomeName(),
|
||||
Code: code,
|
||||
WebBaseAddress: config.WebAddress,
|
||||
})
|
||||
|
|
294
login.go
Normal file
294
login.go
Normal file
|
@ -0,0 +1,294 @@
|
|||
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
|
||||
}
|
226
main.go
226
main.go
|
@ -2,10 +2,8 @@ package main
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
@ -58,7 +56,8 @@ type ConfigFile struct {
|
|||
S3Bucket string `json:"s3_bucket"`
|
||||
}
|
||||
|
||||
var configFlag = flag.String("config", "./config.json", "Configuration file path")
|
||||
var fsServer = flag.NewFlagSet("server", flag.ContinueOnError)
|
||||
var configFlag = fsServer.String("config", "./config.json", "Configuration file path")
|
||||
|
||||
var config *ConfigFile
|
||||
|
||||
|
@ -114,8 +113,25 @@ func getTemplate(name string) *template.Template {
|
|||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if len(os.Args) < 2 {
|
||||
server(os.Args[1:])
|
||||
return
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "cli":
|
||||
cliMain(os.Args[2:])
|
||||
case "server":
|
||||
server(os.Args[2:])
|
||||
default:
|
||||
log.Println("Usage: guichet [server|cli] --help")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func server(args []string) {
|
||||
log.Println("Starting Guichet Server")
|
||||
fsServer.Parse(args)
|
||||
config_file := readConfig()
|
||||
config = &config_file
|
||||
|
||||
|
@ -128,8 +144,12 @@ func main() {
|
|||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", handleHome)
|
||||
r.HandleFunc("/login", handleLogin)
|
||||
r.HandleFunc("/logout", handleLogout)
|
||||
|
||||
r.HandleFunc("/api/unstable/website", handleAPIWebsiteList)
|
||||
r.HandleFunc("/api/unstable/website/{bucket}", handleAPIWebsiteInspect)
|
||||
|
||||
r.HandleFunc("/profile", handleProfile)
|
||||
r.HandleFunc("/passwd", handlePasswd)
|
||||
r.HandleFunc("/picture/{name}", handleDownloadPicture)
|
||||
|
@ -137,10 +157,10 @@ func main() {
|
|||
r.HandleFunc("/directory/search", handleDirectorySearch)
|
||||
r.HandleFunc("/directory", handleDirectory)
|
||||
|
||||
r.HandleFunc("/garage/key", handleGarageKey)
|
||||
r.HandleFunc("/garage/website", handleGarageWebsiteList)
|
||||
r.HandleFunc("/garage/website/new", handleGarageWebsiteNew)
|
||||
r.HandleFunc("/garage/website/b/{bucket}", handleGarageWebsiteInspect)
|
||||
r.HandleFunc("/website", handleWebsiteList)
|
||||
r.HandleFunc("/website/new", handleWebsiteNew)
|
||||
r.HandleFunc("/website/configure", handleWebsiteConfigure)
|
||||
r.HandleFunc("/website/inspect/{bucket}", handleWebsiteInspect)
|
||||
|
||||
r.HandleFunc("/invite/new_account", handleInviteNewAccount)
|
||||
r.HandleFunc("/invite/send_code", handleInviteSendCode)
|
||||
|
@ -163,31 +183,6 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL)
|
||||
|
@ -195,149 +190,31 @@ 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 ----
|
||||
|
||||
// --- Home Controller
|
||||
type HomePageData struct {
|
||||
Login *LoginStatus
|
||||
User *LoggedUser
|
||||
BaseDN string
|
||||
}
|
||||
|
||||
func handleHome(w http.ResponseWriter, r *http.Request) {
|
||||
templateHome := getTemplate("home.html")
|
||||
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := &HomePageData{
|
||||
Login: login,
|
||||
User: user,
|
||||
BaseDN: config.BaseDN,
|
||||
}
|
||||
|
||||
templateHome.Execute(w, data)
|
||||
}
|
||||
|
||||
// --- Logout Controller
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := store.Get(r, SESSION_NAME)
|
||||
if err != nil {
|
||||
|
@ -354,9 +231,10 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
|
||||
// --- Login Controller ---
|
||||
type LoginFormData struct {
|
||||
Username string
|
||||
WrongUser bool
|
||||
|
@ -364,28 +242,26 @@ type LoginFormData struct {
|
|||
ErrorMessage string
|
||||
}
|
||||
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo {
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
templateLogin := getTemplate("login.html")
|
||||
|
||||
if r.Method == "GET" {
|
||||
templateLogin.Execute(w, LoginFormData{})
|
||||
return nil
|
||||
return
|
||||
} else if r.Method == "POST" {
|
||||
r.ParseForm()
|
||||
|
||||
username := strings.Join(r.Form["username"], "")
|
||||
password := strings.Join(r.Form["password"], "")
|
||||
user_dn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, username, config.UserBaseDN)
|
||||
if strings.EqualFold(username, config.AdminAccount) {
|
||||
user_dn = username
|
||||
loginInfo := LoginInfo{username, password}
|
||||
|
||||
l, err := NewLdapCon()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
l := ldapOpen(w)
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := l.Bind(user_dn, password)
|
||||
err = l.Bind(loginInfo.DN(), loginInfo.Password)
|
||||
if err != nil {
|
||||
data := &LoginFormData{
|
||||
Username: username,
|
||||
|
@ -398,7 +274,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo {
|
|||
data.ErrorMessage = err.Error()
|
||||
}
|
||||
templateLogin.Execute(w, data)
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
// Successfully logged in, save it to session
|
||||
|
@ -409,21 +285,15 @@ func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo {
|
|||
|
||||
session.Values["login_username"] = username
|
||||
session.Values["login_password"] = password
|
||||
session.Values["login_dn"] = user_dn
|
||||
|
||||
err = session.Save(r, w)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
return &LoginInfo{
|
||||
DN: user_dn,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
} else {
|
||||
http.Error(w, "Unsupported method", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
10
picture.go
10
picture.go
|
@ -44,7 +44,7 @@ func newMinioClient() (*minio.Client, error) {
|
|||
}
|
||||
|
||||
// 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")
|
||||
|
||||
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
|
||||
// (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+"-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) {
|
||||
name := mux.Vars(r)["name"]
|
||||
|
||||
//Check login
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
// Get user
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
40
profile.go
40
profile.go
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
type ProfileTplData struct {
|
||||
Status *LoginStatus
|
||||
User *LoggedUser
|
||||
ErrorMessage string
|
||||
Success bool
|
||||
Mail string
|
||||
|
@ -23,24 +23,24 @@ type ProfileTplData struct {
|
|||
func handleProfile(w http.ResponseWriter, r *http.Request) {
|
||||
templateProfile := getTemplate("profile.html")
|
||||
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := &ProfileTplData{
|
||||
Status: login,
|
||||
User: user,
|
||||
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.Visibility = login.UserEntry.GetAttributeValue(FIELD_NAME_DIRECTORY_VISIBILITY)
|
||||
data.Description = login.UserEntry.GetAttributeValue("description")
|
||||
data.ProfilePicture = login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE)
|
||||
data.Mail = user.Entry.GetAttributeValue("mail")
|
||||
data.DisplayName = user.Entry.GetAttributeValue("displayname")
|
||||
data.GivenName = user.Entry.GetAttributeValue("givenname")
|
||||
data.Surname = user.Entry.GetAttributeValue("sn")
|
||||
data.Visibility = user.Entry.GetAttributeValue(FIELD_NAME_DIRECTORY_VISIBILITY)
|
||||
data.Description = user.Entry.GetAttributeValue("description")
|
||||
data.ProfilePicture = user.Entry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE)
|
||||
|
||||
if r.Method == "POST" {
|
||||
//5MB maximum size files
|
||||
|
@ -56,7 +56,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
data.Visibility = visible
|
||||
|
||||
profilePicture, err := uploadProfilePicture(w, r, login)
|
||||
profilePicture, err := uploadProfilePicture(w, r, user)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
|
|||
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("givenname", []string{data.GivenName})
|
||||
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})
|
||||
}
|
||||
|
||||
err = login.conn.Modify(modify_request)
|
||||
err = user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
} else {
|
||||
|
@ -88,7 +88,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
type PasswdTplData struct {
|
||||
Status *LoginStatus
|
||||
User *LoggedUser
|
||||
ErrorMessage string
|
||||
TooShortError bool
|
||||
NoMatchError bool
|
||||
|
@ -98,13 +98,13 @@ type PasswdTplData struct {
|
|||
func handlePasswd(w http.ResponseWriter, r *http.Request) {
|
||||
templatePasswd := getTemplate("passwd.html")
|
||||
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := &PasswdTplData{
|
||||
Status: login,
|
||||
User: user,
|
||||
ErrorMessage: "",
|
||||
Success: false,
|
||||
}
|
||||
|
@ -120,11 +120,11 @@ func handlePasswd(w http.ResponseWriter, r *http.Request) {
|
|||
} else if password2 != password {
|
||||
data.NoMatchError = true
|
||||
} else {
|
||||
modify_request := ldap.NewModifyRequest(login.Info.DN, nil)
|
||||
modify_request := ldap.NewModifyRequest(user.Login.Info.DN(), nil)
|
||||
pw, err := SSHAEncode(password)
|
||||
if err == nil {
|
||||
modify_request.Replace("userpassword", []string{pw})
|
||||
err := login.conn.Modify(modify_request)
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
} else {
|
||||
|
|
151
quotas.go
Normal file
151
quotas.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (q *UserQuota) WebsiteSizeAdjust(sz int64) int64 {
|
||||
if sz < q.WebsiteSizeDefault {
|
||||
return q.WebsiteSizeDefault
|
||||
} else if sz > q.WebsiteSizeBursted {
|
||||
return q.WebsiteSizeBursted
|
||||
} else {
|
||||
return sz
|
||||
}
|
||||
}
|
||||
|
||||
func (q *UserQuota) WebsiteObjectAdjust(objs int64) int64 {
|
||||
if objs > q.WebsiteObjects || objs <= 0 {
|
||||
return q.WebsiteObjects
|
||||
} else {
|
||||
return objs
|
||||
}
|
||||
}
|
||||
|
||||
func (q *UserQuota) WebsiteSizeBurstedPretty() string {
|
||||
return prettyValue(q.WebsiteSizeBursted)
|
||||
}
|
||||
|
||||
// --- A quota stat we can use
|
||||
type QuotaStat struct {
|
||||
Current int64 `json:"current"`
|
||||
Max int64 `json:"max"`
|
||||
Ratio float64 `json:"ratio"`
|
||||
Burstable bool `json:"burstable"`
|
||||
}
|
||||
|
||||
func NewQuotaStat(current, max int64, burstable bool) QuotaStat {
|
||||
return QuotaStat{
|
||||
Current: current,
|
||||
Max: max,
|
||||
Ratio: float64(current) / float64(max),
|
||||
Burstable: burstable,
|
||||
}
|
||||
}
|
||||
func (q *QuotaStat) IsFull() bool {
|
||||
return q.Current >= q.Max
|
||||
}
|
||||
func (q *QuotaStat) Percent() int64 {
|
||||
return int64(q.Ratio * 100)
|
||||
}
|
||||
|
||||
func (q *QuotaStat) PrettyCurrent() string {
|
||||
return prettyValue(q.Current)
|
||||
}
|
||||
func (q *QuotaStat) PrettyMax() string {
|
||||
return prettyValue(q.Max)
|
||||
}
|
||||
|
||||
func prettyValue(v int64) string {
|
||||
if v < 1024 {
|
||||
return fmt.Sprintf("%d octets", v)
|
||||
}
|
||||
v = v / 1024
|
||||
if v < 1024 {
|
||||
return fmt.Sprintf("%d kio", v)
|
||||
}
|
||||
v = v / 1024
|
||||
if v < 1024 {
|
||||
return fmt.Sprintf("%d Mio", v)
|
||||
}
|
||||
v = v / 1024
|
||||
if v < 1024 {
|
||||
return fmt.Sprintf("%d Gio", v)
|
||||
}
|
||||
v = v / 1024
|
||||
return fmt.Sprintf("%d Tio", v)
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
{{define "body"}}
|
||||
<div class="d-flex">
|
||||
<h4>Mes identifiants</h4>
|
||||
<a class="ml-auto btn btn-link" href="/garage/website">Mes sites webs</a>
|
||||
<a class="ml-auto btn btn-link" href="/website">Mes sites webs</a>
|
||||
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
|
||||
</div>
|
||||
|
||||
|
@ -21,12 +21,12 @@
|
|||
<table class="table mt-4">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Identifiant de clé</th>
|
||||
<td>{{ .Key.AccessKeyId }}</td>
|
||||
<th scope="row" class="col-md-2">Identifiant de clé</th>
|
||||
<td>{{ .S3KeyInfo.AccessKeyId }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Clé secrète</th>
|
||||
<td>{{ .Key.SecretAccessKey }}</td>
|
||||
<td><a href="#" onclick="document.getElementById('secret_key').style.display='inline'; this.style.display='none'">Cliquer pour afficher la clé secrète</a><span id="secret_key" style="display: none">{{ .S3KeyInfo.SecretAccessKey }}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Région</th>
|
||||
|
@ -58,12 +58,12 @@
|
|||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="awscli" class="collapse show" aria-labelledby="awscli-title" data-parent="#softconfig">
|
||||
<div id="awscli" class="collapse" aria-labelledby="awscli-title" data-parent="#softconfig">
|
||||
<div class="card-body">
|
||||
<p>Créez un fichier nommé <code>~/.awsrc</code> :</p>
|
||||
<pre>
|
||||
export AWS_ACCESS_KEY_ID={{ .Key.AccessKeyId }}
|
||||
export AWS_SECRET_ACCESS_KEY={{ .Key.SecretAccessKey }}
|
||||
export AWS_ACCESS_KEY_ID={{ .S3KeyInfo.AccessKeyId }}
|
||||
export AWS_SECRET_ACCESS_KEY={{ .S3KeyInfo.SecretAccessKey }}
|
||||
export AWS_DEFAULT_REGION='garage'
|
||||
|
||||
function aws { command aws --endpoint-url https://garage.deuxfleurs.fr $@ ; }
|
||||
|
@ -97,8 +97,8 @@ aws s3 cp /tmp/a.txt s3://my-bucket
|
|||
mc alias set \
|
||||
garage \
|
||||
https://garage.deuxfleurs.fr \
|
||||
{{ .Key.AccessKeyId }} \
|
||||
{{ .Key.SecretAccessKey }} \
|
||||
{{ .S3KeyInfo.AccessKeyId }} \
|
||||
{{ .S3KeyInfo.SecretAccessKey }} \
|
||||
--api S3v4
|
||||
</pre>
|
||||
<p>Et ensuite pour utiliser Minio CLI avec :</p>
|
||||
|
@ -176,7 +176,7 @@ hugo deploy
|
|||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Nom d'utilisateur-ice</th>
|
||||
<td>{{ .Status.Info.Username }}</td>
|
||||
<td>{{ .Login.Info.Username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Mot de passe</th>
|
||||
|
@ -207,7 +207,7 @@ hugo deploy
|
|||
<div class="card-body">
|
||||
<p>Un exemple avec SCP :</p>
|
||||
<pre>
|
||||
scp -oHostKeyAlgorithms=+ssh-rsa -P2222 -r ./public {{ .Status.Info.Username }}@bagage.deuxfleurs.fr:mon_bucket/
|
||||
scp -oHostKeyAlgorithms=+ssh-rsa -P2222 -r ./public {{ .Login.Info.Username }}@bagage.deuxfleurs.fr:mon_bucket/
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,57 +2,80 @@
|
|||
|
||||
{{define "body"}}
|
||||
<div class="d-flex">
|
||||
<h4>Inspecter le site web</h4>
|
||||
<a class="ml-auto btn btn-link" href="/garage/key">Mes identifiants</a>
|
||||
<a class="ml-4 btn btn-success" href="/garage/website/new">Nouveau site web</a>
|
||||
<a class="ml-4 btn btn-info" href="/garage/website">Mes sites webs</a>
|
||||
<!--<h4>Inspecter les sites webs</h4>-->
|
||||
<a class="ml-auto btn btn-link" href="/website/configure">Mes identifiants</a>
|
||||
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
|
||||
</div>
|
||||
|
||||
<table class="table mt-4">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">ID</th>
|
||||
<td>{{ .Bucket.Id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">URLs</th>
|
||||
<td>
|
||||
{{ range $alias := .Bucket.GlobalAliases }}
|
||||
{{ if contains $alias "." }}
|
||||
https://{{ $alias }}
|
||||
{{ else }}
|
||||
https://{{ $alias }}.web.deuxfleurs.fr
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Document d'index</th>
|
||||
<td> {{ .IndexDoc }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Document d'erreur</th>
|
||||
<td>{{ .ErrorDoc }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Nombre de fichiers</th>
|
||||
<td>{{ .Bucket.Objects }} / {{ .MaxObjects }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Espace utilisé</th>
|
||||
<td>{{ .Bucket.Bytes }} / {{ .MaxSize }} octets</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row">
|
||||
{{ if .Err }}
|
||||
<div class="col-md-12 mt-3">
|
||||
<div class="alert alert-danger">{{ .Err.Error }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<h4>Configurer le nom de domaine</h4>
|
||||
<div class="col-md-3 mt-3">
|
||||
<a class="btn btn-primary btn-block" href="/website/new">
|
||||
<svg id="i-plus" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="18" height="18" fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" stroke-width="6">
|
||||
<path d="M16 2 L16 30 M2 16 L30 16" />
|
||||
</svg>
|
||||
<span class="ml-1">Nouveau site web</span>
|
||||
</a>
|
||||
|
||||
{{ range $alias := .Bucket.GlobalAliases }}
|
||||
{{ if contains $alias "." }}
|
||||
<p> Le nom de domaine {{ $alias }} n'est pas géré par Deuxfleurs, il vous revient donc de configurer la zone DNS. Vous devez ajouter une entrée <code>CNAME garage.deuxfleurs.fr</code> ou <code>ALIAS garage.deuxfleurs.fr</code> auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).</p>
|
||||
{{ else }}
|
||||
<p> Le nom de domaine https://{{ $alias }}.web.deuxfleurs.fr est fourni par Deuxfleurs, il n'y a pas de configuration à faire.</p>
|
||||
{{ end }}
|
||||
<div class="list-group mt-3">
|
||||
{{ $view := .View }}
|
||||
{{ range $wid := .Describe.Websites }}
|
||||
{{ if eq $wid.Internal $view.Name.Internal }}
|
||||
<a href="/website/inspect/{{ $wid.Pretty }}" class="list-group-item list-group-item-action active">
|
||||
{{ $wid.Url }}
|
||||
</a>
|
||||
{{ else }}
|
||||
<a href="/website/inspect/{{ $wid.Pretty }}" class="list-group-item list-group-item-action">
|
||||
{{ $wid.Url }}
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<p class="text-center mt-2">
|
||||
{{ .Describe.AllowedWebsites.Current }} sites créés sur {{ .Describe.AllowedWebsites.Max }}<br/>
|
||||
Jusqu'à {{ .Describe.BurstBucketQuotaSize }} par site web
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<h2>{{ .View.Name.Url }}</h2>
|
||||
|
||||
<h5 class="mt-3">Quotas</h5>
|
||||
<div class="progress mt-3">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ .View.Size.Current }}" aria-valuemin="0" aria-valuemax="{{ .View.Size.Max }}" style="width: {{ .View.Size.Percent }}%; min-width: 2em;">
|
||||
{{ .View.Size.Ratio }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center">
|
||||
{{ .View.Size.PrettyCurrent }} utilisé sur un maximum de {{ .View.Size.PrettyMax }}
|
||||
{{ if gt .View.Files.Ratio 0.5 }}
|
||||
<br>{{ .View.Files.Current }} fichiers sur un maximum de {{ .View.Files.Max }}
|
||||
{{ end }}
|
||||
</p>
|
||||
|
||||
<h5 class="mt-3">Actions</h5>
|
||||
<form action="" method="post">
|
||||
<div class="btn-group" role="group" aria-label="Actions sur le site web">
|
||||
<button class="btn btn-secondary" name="action" value="increase_quota">Augmenter le quota</button>
|
||||
<a class="btn btn-secondary disabled">Changer le nom de domaine</a>
|
||||
<button class="btn btn-danger" name="action" value="delete_bucket">Supprimer</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
{{ if .View.Name.Expanded }}
|
||||
<h5 class="mt-5">Vous ne savez pas comment configurer votre nom de domaine ?</h5>
|
||||
<p> Le nom de domaine {{ .View.Name.Url }} n'est pas géré par Deuxfleurs, il vous revient donc de configurer la zone DNS. Vous devez ajouter une entrée <code>CNAME garage.deuxfleurs.fr</code> ou <code>ALIAS garage.deuxfleurs.fr</code> auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).</p>
|
||||
{{ end }}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{end}}
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
{{define "title"}}Sites webs |{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
|
||||
<div class="d-flex">
|
||||
<h4>Sites webs</h4>
|
||||
<a class="ml-auto btn btn-link" href="/garage/key">Mes identifiants</a>
|
||||
<a class="ml-4 btn btn-success" href="/garage/website/new">Nouveau site web</a>
|
||||
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
|
||||
</div>
|
||||
|
||||
<table class="table mt-4">
|
||||
<thead>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">URLs</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $buck := .Key.Buckets }}
|
||||
{{ if $buck.GlobalAliases }}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/garage/website/b/{{$buck.Id}}">{{$buck.Id}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ range $alias := $buck.GlobalAliases }}
|
||||
{{ if contains $alias "." }}
|
||||
https://{{ $alias }}
|
||||
{{ else }}
|
||||
https://{{ $alias }}.web.deuxfleurs.fr
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
|
@ -3,8 +3,16 @@
|
|||
{{define "body"}}
|
||||
<div class="d-flex">
|
||||
<h4>Créer un site web</h4>
|
||||
<a class="ml-auto btn btn-link" href="/garage/key">Mes identifiants</a>
|
||||
<a class="ml-4 btn btn-info" href="/garage/website">Mes sites webs</a>
|
||||
<a class="ml-auto btn btn-link" href="/website/configure">Mes identifiants</a>
|
||||
<a class="ml-4 btn btn-info" href="/website">Mes sites webs</a>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-12">
|
||||
{{if .Err}}
|
||||
<div class="alert alert-danger">{{ .Err.Error }}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" id="proto" role="tablist">
|
||||
|
@ -17,6 +25,7 @@
|
|||
</ul>
|
||||
|
||||
<div class="tab-content" id="protocols">
|
||||
|
||||
<div class="tab-pane fade show active" id="dnsint" role="tabpanel" aria-labelledby="dnsint-tab">
|
||||
<form method="POST" class="mt-4">
|
||||
<div class="form-row">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{{define "body"}}
|
||||
<div class="alert alert-info">
|
||||
Bienvenue, <strong>{{ .Login.WelcomeName }}</strong> !
|
||||
Bienvenue, <strong>{{ .User.WelcomeName }}</strong> !
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a class="ml-auto btn btn-sm btn-dark" href="/logout">Se déconnecter</a>
|
||||
|
@ -24,16 +24,16 @@
|
|||
<div class="mt-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Garage
|
||||
Mon espace sur la toile
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action" href="/garage/key">Mes identifiants</a>
|
||||
<a class="list-group-item list-group-item-action" href="/garage/website">Mes sites webs</a>
|
||||
<a class="list-group-item list-group-item-action" href="/website/configure">Mes identifiants</a>
|
||||
<a class="list-group-item list-group-item-action" href="/website">Mes sites Web</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .Login.CanInvite}}
|
||||
{{if .User.Capabilities.CanInvite}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
Inviter des gens sur Deuxfleurs
|
||||
|
@ -45,7 +45,7 @@
|
|||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Login.CanAdmin}}
|
||||
{{if .User.Capabilities.CanAdmin}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
Administration
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label>Identifiant:</label>
|
||||
<input type="text" disabled="true" class="form-control" value="{{ .Status.Info.Username }}" />
|
||||
<input type="text" disabled="true" class="form-control" value="{{ .User.Login.Info.Username }}" />
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="mail">Adresse e-mail:</label>
|
||||
|
|
236
website.go
Normal file
236
website.go
Normal file
|
@ -0,0 +1,236 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWebsiteNotFound = fmt.Errorf("Website not found")
|
||||
ErrFetchBucketInfo = fmt.Errorf("Failed to fetch bucket information")
|
||||
ErrWebsiteQuotaReached = fmt.Errorf("Can't create additional websites, quota reached")
|
||||
ErrEmptyBucketName = fmt.Errorf("You can't create a website with an empty name")
|
||||
ErrCantCreateBucket = fmt.Errorf("Can't create this bucket. Maybe another bucket already exists with this name or you have an invalid character")
|
||||
ErrCantAllowKey = fmt.Errorf("Can't allow given key on the target bucket")
|
||||
ErrCantConfigureBucket = fmt.Errorf("Unable to configure the bucket (activating website, adding quotas, etc.)")
|
||||
ErrBucketDeleteNotEmpty = fmt.Errorf("You must remove all the files before deleting a bucket")
|
||||
ErrBucketDeleteUnfinishedUpload = fmt.Errorf("You must remove all the unfinished multipart uploads before deleting a bucket")
|
||||
)
|
||||
|
||||
type WebsiteId struct {
|
||||
Pretty string `json:"name"`
|
||||
Internal string `json:"-"`
|
||||
Alt []string `json:"alt_name"`
|
||||
Expanded bool `json:"expanded"`
|
||||
Url string `json:"domain"`
|
||||
}
|
||||
|
||||
func NewWebsiteId(id string, aliases []string) *WebsiteId {
|
||||
pretty := id
|
||||
var alt []string
|
||||
if len(aliases) > 0 {
|
||||
pretty = aliases[0]
|
||||
alt = aliases[1:]
|
||||
}
|
||||
expanded := strings.Contains(pretty, ".")
|
||||
|
||||
url := pretty
|
||||
if !expanded {
|
||||
url = fmt.Sprintf("%s.web.deuxfleurs.fr", pretty)
|
||||
}
|
||||
|
||||
return &WebsiteId{pretty, id, alt, expanded, url}
|
||||
}
|
||||
func NewWebsiteIdFromBucketInfo(binfo *garage.BucketInfo) *WebsiteId {
|
||||
return NewWebsiteId(*binfo.Id, binfo.GlobalAliases)
|
||||
}
|
||||
|
||||
type WebsiteController struct {
|
||||
User *LoggedUser
|
||||
WebsiteIdx map[string]*WebsiteId
|
||||
PrettyList []string
|
||||
WebsiteCount QuotaStat
|
||||
}
|
||||
|
||||
func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) {
|
||||
idx := map[string]*WebsiteId{}
|
||||
var wlist []string
|
||||
|
||||
keyInfo, err := user.S3KeyInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, bckt := range keyInfo.Buckets {
|
||||
if len(bckt.GlobalAliases) > 0 {
|
||||
wid := NewWebsiteId(*bckt.Id, bckt.GlobalAliases)
|
||||
idx[wid.Pretty] = wid
|
||||
wlist = append(wlist, wid.Pretty)
|
||||
}
|
||||
}
|
||||
sort.Strings(wlist)
|
||||
|
||||
maxW := user.Quota.WebsiteCount
|
||||
quota := NewQuotaStat(int64(len(wlist)), maxW, true)
|
||||
|
||||
return &WebsiteController{user, idx, wlist, quota}, nil
|
||||
}
|
||||
|
||||
type WebsiteDescribe struct {
|
||||
AccessKeyId string `json:"access_key_id"`
|
||||
SecretAccessKey string `json:"secret_access_key"`
|
||||
AllowedWebsites *QuotaStat `json:"quota_website_count"`
|
||||
BurstBucketQuotaSize string `json:"burst_bucket_quota_size"`
|
||||
Websites []*WebsiteId `json:"vhosts"`
|
||||
}
|
||||
|
||||
func (w *WebsiteController) Describe() (*WebsiteDescribe, error) {
|
||||
s3key, err := w.User.S3KeyInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := make([]*WebsiteId, 0, len(w.PrettyList))
|
||||
for _, k := range w.PrettyList {
|
||||
r = append(r, w.WebsiteIdx[k])
|
||||
}
|
||||
return &WebsiteDescribe{
|
||||
*s3key.AccessKeyId,
|
||||
*s3key.SecretAccessKey,
|
||||
&w.WebsiteCount,
|
||||
w.User.Quota.WebsiteSizeBurstedPretty(),
|
||||
r}, nil
|
||||
}
|
||||
|
||||
func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) {
|
||||
website, ok := w.WebsiteIdx[pretty]
|
||||
if !ok {
|
||||
return nil, ErrWebsiteNotFound
|
||||
}
|
||||
|
||||
binfo, err := grgGetBucket(website.Internal)
|
||||
if err != nil {
|
||||
return nil, ErrFetchBucketInfo
|
||||
}
|
||||
|
||||
return NewWebsiteView(binfo), nil
|
||||
}
|
||||
|
||||
func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteView, error) {
|
||||
website, ok := w.WebsiteIdx[pretty]
|
||||
if !ok {
|
||||
return nil, ErrWebsiteNotFound
|
||||
}
|
||||
|
||||
binfo, err := grgGetBucket(website.Internal)
|
||||
if err != nil {
|
||||
return nil, ErrFetchBucketInfo
|
||||
}
|
||||
|
||||
// Patch the max size
|
||||
urQuota := garage.NewUpdateBucketRequestQuotas()
|
||||
urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(binfo.Quotas.GetMaxSize()))
|
||||
urQuota.SetMaxObjects(w.User.Quota.WebsiteObjectAdjust(binfo.Quotas.GetMaxObjects()))
|
||||
if patch.Size != nil {
|
||||
urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(*patch.Size))
|
||||
}
|
||||
|
||||
// Build the update
|
||||
ur := garage.NewUpdateBucketRequest()
|
||||
ur.SetQuotas(*urQuota)
|
||||
|
||||
// Call garage
|
||||
binfo, err = grgUpdateBucket(website.Internal, ur)
|
||||
if err != nil {
|
||||
return nil, ErrCantConfigureBucket
|
||||
}
|
||||
|
||||
return NewWebsiteView(binfo), nil
|
||||
}
|
||||
|
||||
func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) {
|
||||
if pretty == "" {
|
||||
return nil, ErrEmptyBucketName
|
||||
}
|
||||
|
||||
if w.WebsiteCount.IsFull() {
|
||||
return nil, ErrWebsiteQuotaReached
|
||||
}
|
||||
|
||||
binfo, err := grgCreateBucket(pretty)
|
||||
if err != nil {
|
||||
return nil, ErrCantCreateBucket
|
||||
}
|
||||
|
||||
s3key, err := w.User.S3KeyInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
binfo, err = grgAllowKeyOnBucket(*binfo.Id, *s3key.AccessKeyId)
|
||||
if err != nil {
|
||||
return nil, ErrCantAllowKey
|
||||
}
|
||||
|
||||
qr := w.User.Quota.DefaultWebsiteQuota()
|
||||
wr := allowWebsiteDefault()
|
||||
|
||||
ur := garage.NewUpdateBucketRequest()
|
||||
ur.SetWebsiteAccess(*wr)
|
||||
ur.SetQuotas(*qr)
|
||||
|
||||
binfo, err = grgUpdateBucket(*binfo.Id, ur)
|
||||
if err != nil {
|
||||
return nil, ErrCantConfigureBucket
|
||||
}
|
||||
|
||||
return NewWebsiteView(binfo), nil
|
||||
}
|
||||
|
||||
func (w *WebsiteController) Delete(pretty string) error {
|
||||
if pretty == "" {
|
||||
return ErrEmptyBucketName
|
||||
}
|
||||
|
||||
website, ok := w.WebsiteIdx[pretty]
|
||||
if !ok {
|
||||
return ErrWebsiteNotFound
|
||||
}
|
||||
|
||||
binfo, err := grgGetBucket(website.Internal)
|
||||
if err != nil {
|
||||
return ErrFetchBucketInfo
|
||||
}
|
||||
|
||||
if *binfo.Objects > int64(0) {
|
||||
return ErrBucketDeleteNotEmpty
|
||||
}
|
||||
|
||||
if *binfo.UnfinishedUploads > int32(0) {
|
||||
return ErrBucketDeleteUnfinishedUpload
|
||||
}
|
||||
|
||||
err = grgDeleteBucket(website.Internal)
|
||||
return err
|
||||
}
|
||||
|
||||
type WebsiteView struct {
|
||||
Name *WebsiteId `json:"identified_as"`
|
||||
Size QuotaStat `json:"quota_size"`
|
||||
Files QuotaStat `json:"quota_files"`
|
||||
}
|
||||
|
||||
func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView {
|
||||
q := binfo.GetQuotas()
|
||||
|
||||
wid := NewWebsiteIdFromBucketInfo(binfo)
|
||||
size := NewQuotaStat(*binfo.Bytes, (&q).GetMaxSize(), true)
|
||||
objects := NewQuotaStat(*binfo.Objects, (&q).GetMaxObjects(), false)
|
||||
return &WebsiteView{wid, size, objects}
|
||||
}
|
||||
|
||||
type WebsitePatch struct {
|
||||
Size *int64 `json:"quota_size"`
|
||||
}
|
Loading…
Reference in a new issue