Compare commits
4 Commits
Author | SHA1 | Date |
---|---|---|
thiesjohannbroetje | 150aa37cc1 | |
thiesjohannbroetje | 8c02d4f827 | |
thiesjohannbroetje | 4f207e6ed5 | |
thiesjohannbroetje | 4c7250df42 |
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
pipeline:
|
||||
build:
|
||||
image: golang:stretch
|
||||
commands:
|
||||
- go get -d -v
|
||||
- go build -v
|
||||
|
||||
---
|
||||
kind: signature
|
||||
hmac: 38948cd073f3a0b73ab7bb13ba1b5e18c64c02976abfd6dcd5bf7a4c34197e8c
|
||||
|
||||
...
|
|
@ -1,6 +1,3 @@
|
|||
guichet
|
||||
guichet.static
|
||||
config.json
|
||||
result
|
||||
.direnv/
|
||||
password
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
- tag
|
||||
- cron
|
||||
- manual
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
commands:
|
||||
- nix build --extra-experimental-features nix-command --extra-experimental-features flakes .
|
|
@ -1,6 +1,6 @@
|
|||
# Guichet
|
||||
|
||||
[![status-badge](https://woodpecker.deuxfleurs.fr/api/badges/37/status.svg)](https://woodpecker.deuxfleurs.fr/repos/37)
|
||||
[![Build Status](https://drone.deuxfleurs.fr/api/badges/Deuxfleurs/guichet/status.svg?ref=refs/heads/main)](https://drone.deuxfleurs.fr/Deuxfleurs/guichet)
|
||||
|
||||
Guichet is a simple LDAP web interface for the following tasks:
|
||||
|
||||
|
|
345
admin.go
345
admin.go
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
@ -11,18 +12,18 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func checkAdminLogin(w http.ResponseWriter, r *http.Request) *LoggedUser {
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
func checkAdminLogin(w http.ResponseWriter, r *http.Request) *LoginStatus {
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !user.Capabilities.CanAdmin {
|
||||
if !login.CanAdmin {
|
||||
http.Error(w, "Not authorized to perform administrative operations.", http.StatusUnauthorized)
|
||||
return nil
|
||||
}
|
||||
|
||||
return user
|
||||
return login
|
||||
}
|
||||
|
||||
type EntryList []*ldap.Entry
|
||||
|
@ -40,17 +41,17 @@ func (d EntryList) Less(i, j int) bool {
|
|||
}
|
||||
|
||||
type AdminUsersTplData struct {
|
||||
User *LoggedUser
|
||||
Login *LoginStatus
|
||||
UserNameAttr string
|
||||
UserBaseDN string
|
||||
Users EntryList
|
||||
}
|
||||
|
||||
func handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
||||
templateAdminUsers := getTemplate("admin_users.html")
|
||||
templateAdminUsers := template.Must(template.ParseFiles("templates/layout.html", "templates/admin_users.html"))
|
||||
|
||||
user := checkAdminLogin(w, r)
|
||||
if user == nil {
|
||||
login := checkAdminLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -61,14 +62,14 @@ func handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
|||
[]string{config.UserNameAttr, "dn", "displayname", "givenname", "sn", "mail"},
|
||||
nil)
|
||||
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := &AdminUsersTplData{
|
||||
User: user,
|
||||
Login: login,
|
||||
UserNameAttr: config.UserNameAttr,
|
||||
UserBaseDN: config.UserBaseDN,
|
||||
Users: EntryList(sr.Entries),
|
||||
|
@ -79,17 +80,17 @@ func handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
type AdminGroupsTplData struct {
|
||||
User *LoggedUser
|
||||
Login *LoginStatus
|
||||
GroupNameAttr string
|
||||
GroupBaseDN string
|
||||
Groups EntryList
|
||||
}
|
||||
|
||||
func handleAdminGroups(w http.ResponseWriter, r *http.Request) {
|
||||
templateAdminGroups := getTemplate("admin_groups.html")
|
||||
templateAdminGroups := template.Must(template.ParseFiles("templates/layout.html", "templates/admin_groups.html"))
|
||||
|
||||
user := checkAdminLogin(w, r)
|
||||
if user == nil {
|
||||
login := checkAdminLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -100,14 +101,14 @@ func handleAdminGroups(w http.ResponseWriter, r *http.Request) {
|
|||
[]string{config.GroupNameAttr, "dn", "description"},
|
||||
nil)
|
||||
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := &AdminGroupsTplData{
|
||||
User: user,
|
||||
Login: login,
|
||||
GroupNameAttr: config.GroupNameAttr,
|
||||
GroupBaseDN: config.GroupBaseDN,
|
||||
Groups: EntryList(sr.Entries),
|
||||
|
@ -117,242 +118,14 @@ func handleAdminGroups(w http.ResponseWriter, r *http.Request) {
|
|||
templateAdminGroups.Execute(w, data)
|
||||
}
|
||||
|
||||
type AdminMailingTplData struct {
|
||||
User *LoggedUser
|
||||
MailingNameAttr string
|
||||
MailingBaseDN string
|
||||
MailingLists EntryList
|
||||
}
|
||||
|
||||
func handleAdminMailing(w http.ResponseWriter, r *http.Request) {
|
||||
templateAdminMailing := getTemplate("admin_mailing.html")
|
||||
|
||||
user := checkAdminLogin(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
config.MailingBaseDN,
|
||||
ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(&(objectClass=groupOfNames))"),
|
||||
[]string{config.MailingNameAttr, "dn", "description"},
|
||||
nil)
|
||||
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := &AdminMailingTplData{
|
||||
User: user,
|
||||
MailingNameAttr: config.MailingNameAttr,
|
||||
MailingBaseDN: config.MailingBaseDN,
|
||||
MailingLists: EntryList(sr.Entries),
|
||||
}
|
||||
sort.Sort(data.MailingLists)
|
||||
|
||||
templateAdminMailing.Execute(w, data)
|
||||
}
|
||||
|
||||
type AdminMailingListTplData struct {
|
||||
User *LoggedUser
|
||||
MailingNameAttr string
|
||||
MailingBaseDN string
|
||||
|
||||
MailingList *ldap.Entry
|
||||
Members EntryList
|
||||
PossibleNewMembers EntryList
|
||||
AllowGuest bool
|
||||
|
||||
Error string
|
||||
Success bool
|
||||
}
|
||||
|
||||
func handleAdminMailingList(w http.ResponseWriter, r *http.Request) {
|
||||
templateAdminMailingList := getTemplate("admin_mailing_list.html")
|
||||
|
||||
user := checkAdminLogin(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
dn := fmt.Sprintf("%s=%s,%s", config.MailingNameAttr, id, config.MailingBaseDN)
|
||||
|
||||
// handle modifications
|
||||
dError := ""
|
||||
dSuccess := false
|
||||
|
||||
if r.Method == "POST" {
|
||||
r.ParseForm()
|
||||
action := strings.Join(r.Form["action"], "")
|
||||
if action == "add-member" {
|
||||
member := strings.Join(r.Form["member"], "")
|
||||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Add("member", []string{member})
|
||||
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
dSuccess = true
|
||||
}
|
||||
} else if action == "add-external" {
|
||||
mail := strings.Join(r.Form["mail"], "")
|
||||
displayname := strings.Join(r.Form["displayname"], "")
|
||||
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
config.UserBaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(&(objectClass=organizationalPerson)(mail=%s))", mail),
|
||||
[]string{"dn", "displayname", "mail"},
|
||||
nil)
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
if len(sr.Entries) == 0 {
|
||||
if config.MailingGuestsBaseDN != "" {
|
||||
guestDn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, mail, config.MailingGuestsBaseDN)
|
||||
req := ldap.NewAddRequest(guestDn, nil)
|
||||
req.Attribute("objectclass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"})
|
||||
req.Attribute("mail", []string{mail})
|
||||
if displayname != "" {
|
||||
req.Attribute("displayname", []string{displayname})
|
||||
}
|
||||
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 := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
dSuccess = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dError = "Adding guest users not supported, the user must already have an LDAP account."
|
||||
}
|
||||
} else if len(sr.Entries) == 1 {
|
||||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Add("member", []string{sr.Entries[0].DN})
|
||||
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
dSuccess = true
|
||||
}
|
||||
} else {
|
||||
dError = fmt.Sprintf("Multiple users exist with email address %s", mail)
|
||||
}
|
||||
}
|
||||
} else if action == "delete-member" {
|
||||
member := strings.Join(r.Form["member"], "")
|
||||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Delete("member", []string{member})
|
||||
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
dSuccess = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve mailing list
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
dn,
|
||||
ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(objectclass=groupOfNames)"),
|
||||
[]string{"dn", config.MailingNameAttr, "member", "description"},
|
||||
nil)
|
||||
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(sr.Entries) != 1 {
|
||||
http.Error(w, fmt.Sprintf("Object not found: %s", dn), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ml := sr.Entries[0]
|
||||
|
||||
memberDns := make(map[string]bool)
|
||||
for _, attr := range ml.Attributes {
|
||||
if attr.Name == "member" {
|
||||
for _, v := range attr.Values {
|
||||
memberDns[v] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve list of current and possible new members
|
||||
members := []*ldap.Entry{}
|
||||
possibleNewMembers := []*ldap.Entry{}
|
||||
|
||||
searchRequest = ldap.NewSearchRequest(
|
||||
config.UserBaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(objectClass=organizationalPerson)"),
|
||||
[]string{"dn", "displayname", "mail"},
|
||||
nil)
|
||||
sr, err = user.Login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, ent := range sr.Entries {
|
||||
if _, ok := memberDns[ent.DN]; ok {
|
||||
members = append(members, ent)
|
||||
} else {
|
||||
possibleNewMembers = append(possibleNewMembers, ent)
|
||||
}
|
||||
}
|
||||
|
||||
data := &AdminMailingListTplData{
|
||||
User: user,
|
||||
MailingNameAttr: config.MailingNameAttr,
|
||||
MailingBaseDN: config.MailingBaseDN,
|
||||
|
||||
MailingList: ml,
|
||||
Members: members,
|
||||
PossibleNewMembers: possibleNewMembers,
|
||||
AllowGuest: config.MailingGuestsBaseDN != "",
|
||||
|
||||
Error: dError,
|
||||
Success: dSuccess,
|
||||
}
|
||||
sort.Sort(data.Members)
|
||||
sort.Sort(data.PossibleNewMembers)
|
||||
|
||||
templateAdminMailingList.Execute(w, data)
|
||||
}
|
||||
|
||||
// ===================================================
|
||||
// LDAP EXPLORER
|
||||
// ===================================================
|
||||
|
||||
type AdminLDAPTplData struct {
|
||||
DN string
|
||||
|
||||
Path []PathItem
|
||||
ChildrenOU []Child
|
||||
ChildrenOther []Child
|
||||
CanAddChild bool
|
||||
Props map[string]*PropValues
|
||||
CanDelete bool
|
||||
Path []PathItem
|
||||
Children []Child
|
||||
CanAddChild bool
|
||||
Props map[string]*PropValues
|
||||
CanDelete bool
|
||||
|
||||
HasMembers bool
|
||||
Members []EntryName
|
||||
|
@ -392,10 +165,10 @@ type PropValues struct {
|
|||
}
|
||||
|
||||
func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
||||
templateAdminLDAP := getTemplate("admin_ldap.html")
|
||||
templateAdminLDAP := template.Must(template.ParseFiles("templates/layout.html", "templates/admin_ldap.html"))
|
||||
|
||||
user := checkAdminLogin(w, r)
|
||||
if user == nil {
|
||||
login := checkAdminLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -445,7 +218,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Replace(attr, values_filtered)
|
||||
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
err := login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -466,7 +239,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Add(attr, values_filtered)
|
||||
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
err := login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -478,7 +251,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Replace(attr, []string{})
|
||||
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
err := login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -489,7 +262,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(group, nil)
|
||||
modify_request.Delete("member", []string{dn})
|
||||
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
err := login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -500,7 +273,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(group, nil)
|
||||
modify_request.Add("member", []string{dn})
|
||||
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
err := login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -511,7 +284,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request := ldap.NewModifyRequest(dn, nil)
|
||||
modify_request.Delete("member", []string{member})
|
||||
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
err := login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -519,7 +292,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
} else if action == "delete-object" {
|
||||
del_request := ldap.NewDelRequest(dn, nil)
|
||||
err := user.Login.conn.Del(del_request)
|
||||
err := login.conn.Del(del_request)
|
||||
if err != nil {
|
||||
dError = err.Error()
|
||||
} else {
|
||||
|
@ -537,7 +310,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
[]string{},
|
||||
nil)
|
||||
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -621,7 +394,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
fmt.Sprintf("(objectClass=organizationalPerson)"),
|
||||
[]string{"dn", "displayname", "description"},
|
||||
nil)
|
||||
sr, err = user.Login.conn.Search(searchRequest)
|
||||
sr, err = login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -675,7 +448,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
fmt.Sprintf("(objectClass=groupOfNames)"),
|
||||
[]string{"dn", "description"},
|
||||
nil)
|
||||
sr, err = user.Login.conn.Search(searchRequest)
|
||||
sr, err = login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -719,7 +492,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
[]string{"dn", "displayname", "description"},
|
||||
nil)
|
||||
|
||||
sr, err = user.Login.conn.Search(searchRequest)
|
||||
sr, err = login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -727,35 +500,28 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
sort.Sort(EntryList(sr.Entries))
|
||||
|
||||
childrenOU := []Child{}
|
||||
childrenOther := []Child{}
|
||||
children := []Child{}
|
||||
for _, item := range sr.Entries {
|
||||
name := item.GetAttributeValue("displayname")
|
||||
if name == "" {
|
||||
name = item.GetAttributeValue("description")
|
||||
}
|
||||
child := Child{
|
||||
children = append(children, Child{
|
||||
DN: item.DN,
|
||||
Identifier: strings.Split(item.DN, ",")[0],
|
||||
Name: name,
|
||||
}
|
||||
if strings.HasPrefix(item.DN, "ou=") {
|
||||
childrenOU = append(childrenOU, child)
|
||||
} else {
|
||||
childrenOther = append(childrenOther, child)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Run template, finally!
|
||||
templateAdminLDAP.Execute(w, &AdminLDAPTplData{
|
||||
DN: dn,
|
||||
|
||||
Path: path,
|
||||
ChildrenOU: childrenOU,
|
||||
ChildrenOther: childrenOther,
|
||||
Props: props,
|
||||
CanAddChild: dn_last_attr == "ou" || isOrganization,
|
||||
CanDelete: dn != config.BaseDN && len(childrenOU) == 0 && len(childrenOther) == 0,
|
||||
Path: path,
|
||||
Children: children,
|
||||
Props: props,
|
||||
CanAddChild: dn_last_attr == "ou" || isOrganization,
|
||||
CanDelete: dn != config.BaseDN && len(children) == 0,
|
||||
|
||||
HasMembers: len(members) > 0 || hasMembers,
|
||||
Members: members,
|
||||
|
@ -785,10 +551,10 @@ type CreateData struct {
|
|||
}
|
||||
|
||||
func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
|
||||
templateAdminCreate := getTemplate("admin_create.html")
|
||||
templateAdminCreate := template.Must(template.ParseFiles("templates/layout.html", "templates/admin_create.html"))
|
||||
|
||||
user := checkAdminLogin(w, r)
|
||||
if user == nil {
|
||||
login := checkAdminLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -803,7 +569,7 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
|
|||
[]string{},
|
||||
nil)
|
||||
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -841,7 +607,7 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
|
|||
data.IdType = config.UserNameAttr
|
||||
data.StructuralObjectClass = "inetOrgPerson"
|
||||
data.ObjectClass = "inetOrgPerson\norganizationalPerson\nperson\ntop"
|
||||
} else if template == "group" || template == "ml" {
|
||||
} else if template == "group" {
|
||||
data.IdType = config.UserNameAttr
|
||||
data.StructuralObjectClass = "groupOfNames"
|
||||
data.ObjectClass = "groupOfNames\ntop"
|
||||
|
@ -880,6 +646,8 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
|
|||
data.Error = "Invalid identifier type"
|
||||
} else if len(data.IdValue) == 0 {
|
||||
data.Error = "No identifier specified"
|
||||
} else if match, err := regexp.MatchString("^[\\d\\w_-]+$", data.IdValue); err != nil || !match {
|
||||
data.Error = "Invalid identifier"
|
||||
} else {
|
||||
dn := data.IdType + "=" + data.IdValue + "," + super_dn
|
||||
req := ldap.NewAddRequest(dn, nil)
|
||||
|
@ -894,16 +662,13 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) {
|
|||
req.Attribute("description", []string{data.Description})
|
||||
}
|
||||
|
||||
err := user.Login.conn.Add(req)
|
||||
err := login.conn.Add(req)
|
||||
if err != nil {
|
||||
data.Error = err.Error()
|
||||
} else {
|
||||
if template == "ml" {
|
||||
http.Redirect(w, r, "/admin/mailing/"+data.IdValue, http.StatusFound)
|
||||
} else {
|
||||
http.Redirect(w, r, "/admin/ldap/"+dn, http.StatusFound)
|
||||
}
|
||||
http.Redirect(w, r, "/admin/ldap/"+dn, http.StatusFound)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
127
api.go
127
api.go
|
@ -1,127 +0,0 @@
|
|||
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
44
cli.go
|
@ -1,44 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -2,33 +2,30 @@
|
|||
"http_bind_addr": ":9991",
|
||||
"ldap_server_addr": "ldap://127.0.0.1:389",
|
||||
|
||||
"base_dn": "dc=bottin,dc=eu",
|
||||
"user_base_dn": "ou=users,dc=bottin,dc=eu",
|
||||
"user_name_attr": "cn",
|
||||
"group_base_dn": "ou=groups,dc=bottin,dc=eu",
|
||||
"group_name_attr": "cn",
|
||||
"base_dn": "dc=example,dc=com",
|
||||
"user_base_dn": "ou=users,dc=example,dc=com",
|
||||
"user_name_attr": "uid",
|
||||
"group_base_dn": "ou=groups,dc=example,dc=com",
|
||||
"group_name_attr": "gid",
|
||||
|
||||
"invitation_base_dn": "ou=invitations,dc=bottin,dc=eu",
|
||||
"invitation_base_dn": "ou=invitations,dc=example,dc=com",
|
||||
"invitation_name_attr": "cn",
|
||||
"invited_mail_format": "{}@example.com",
|
||||
"invited_auto_groups": [
|
||||
"cn=email,ou=groups,dc=bottin,dc=eu"
|
||||
"cn=email,ou=groups,dc=example,dc=com"
|
||||
],
|
||||
|
||||
"web_address": "http://guichet.localhost:9991",
|
||||
"web_address": "https://guichet.example.com",
|
||||
"mail_from": "welcome@example.com",
|
||||
"smtp_server": "smtp.example.com",
|
||||
"smtp_username": "guichet",
|
||||
"smtp_password": "",
|
||||
|
||||
"admin_account": "cn=admin,dc=bottin,dc=eu",
|
||||
"group_can_admin": "gid=admin,ou=groups,dc=bottin,dc=eu",
|
||||
"admin_account": "uid=admin,dc=example,dc=com",
|
||||
"group_can_admin": "gid=admin,ou=groups,dc=example,dc=com",
|
||||
"group_can_invite": "",
|
||||
|
||||
"s3_admin_endpoint": "localhost:3903",
|
||||
"s3_admin_token": "GlXP43PWH3LuvEGSNxKYzZCyUss8VqZmarBU+HUlrxw=",
|
||||
|
||||
"s3_endpoint": "localhost",
|
||||
"s3_endpoint": "garage.example.com",
|
||||
"s3_access_key": "",
|
||||
"s3_secret_key": "",
|
||||
"s3_region": "garage",
|
||||
|
|
14
directory.go
14
directory.go
|
@ -13,10 +13,10 @@ const FIELD_NAME_PROFILE_PICTURE = "profilePicture"
|
|||
const FIELD_NAME_DIRECTORY_VISIBILITY = "directoryVisibility"
|
||||
|
||||
func handleDirectory(w http.ResponseWriter, r *http.Request) {
|
||||
templateDirectory := getTemplate("directory.html")
|
||||
templateDirectory := template.Must(template.ParseFiles("templates/layout.html", "templates/directory.html"))
|
||||
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ type SearchResults struct {
|
|||
}
|
||||
|
||||
func handleDirectorySearch(w http.ResponseWriter, r *http.Request) {
|
||||
templateDirectoryResults := template.Must(template.ParseFiles(templatePath + "/directory_results.html"))
|
||||
templateDirectoryResults := template.Must(template.ParseFiles("templates/directory_results.html"))
|
||||
|
||||
//Get input value by user
|
||||
r.ParseMultipartForm(1024)
|
||||
|
@ -49,8 +49,8 @@ func handleDirectorySearch(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
//Log to allow the research
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
http.Error(w, "Login required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ func handleDirectorySearch(w http.ResponseWriter, r *http.Request) {
|
|||
},
|
||||
nil)
|
||||
|
||||
sr, err := user.Login.conn.Search(searchRequest)
|
||||
sr, err := login.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
|
97
flake.lock
97
flake.lock
|
@ -1,97 +0,0 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gomod2nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694616124,
|
||||
"narHash": "sha256-c49BVhQKw3XDRgt+y+uPAbArtgUlMXCET6VxEBmzHXE=",
|
||||
"owner": "tweag",
|
||||
"repo": "gomod2nix",
|
||||
"rev": "f95720e89af6165c8c0aa77f180461fe786f3c21",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "tweag",
|
||||
"repo": "gomod2nix",
|
||||
"rev": "f95720e89af6165c8c0aa77f180461fe786f3c21",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1695711119,
|
||||
"narHash": "sha256-qrtJ4zliGgH24FMhj5a/5Gq7SkjqKquF5AVS0eEevBk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "044b1b65fd5dd49a535e9a9bd1a2cee884eb22d6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "master",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1695710730,
|
||||
"narHash": "sha256-GigCuk3t8AVXr2NdX6eBgouc20JWWrZatbKH3xZZSC4=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f758d66c9cc3011f5327f8583908a7803cc019b1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f758d66c9cc3011f5327f8583908a7803cc019b1",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"gomod2nix": "gomod2nix",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
54
flake.nix
54
flake.nix
|
@ -1,54 +0,0 @@
|
|||
{
|
||||
description = "A simple LDAP web interface for Bottin";
|
||||
|
||||
inputs.nixpkgs.url =
|
||||
"github:nixos/nixpkgs/f758d66c9cc3011f5327f8583908a7803cc019b1";
|
||||
inputs.gomod2nix.url =
|
||||
"github:tweag/gomod2nix/f95720e89af6165c8c0aa77f180461fe786f3c21";
|
||||
|
||||
outputs = { self, nixpkgs, gomod2nix }:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
system = "x86_64-linux";
|
||||
overlays = [
|
||||
(import "${gomod2nix}/overlay.nix")
|
||||
/*(self: super: {
|
||||
gomod = super.callPackage "${gomod2nix}/builder/" { };
|
||||
})*/
|
||||
];
|
||||
};
|
||||
src = ./.;
|
||||
guichet = pkgs.buildGoApplication {
|
||||
pname = "guichet";
|
||||
version = "0.1.0";
|
||||
src = src;
|
||||
modules = ./gomod2nix.toml;
|
||||
|
||||
CGO_ENABLED = 0;
|
||||
|
||||
ldflags = [
|
||||
"-X main.templatePath=${src + "/templates"}"
|
||||
"-X main.staticPath=${src + "/static"}"
|
||||
];
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "A simple LDAP web interface for Bottin";
|
||||
homepage = "https://git.deuxfleurs.fr/Deuxfleurs/guichet";
|
||||
license = licenses.gpl3Plus;
|
||||
platforms = platforms.linux;
|
||||
};
|
||||
};
|
||||
container = pkgs.dockerTools.buildImage {
|
||||
name = "dxflrs/guichet";
|
||||
config = {
|
||||
Entrypoint = "${guichet}/bin/guichet";
|
||||
};
|
||||
};
|
||||
in {
|
||||
packages.x86_64-linux.guichet = guichet;
|
||||
packages.x86_64-linux.container = container;
|
||||
packages.x86_64-linux.default = guichet;
|
||||
|
||||
devShell.x86_64-linux = pkgs.mkShell { nativeBuildInputs = [ pkgs.go pkgs.gomod2nix ]; };
|
||||
};
|
||||
}
|
346
garage.go
346
garage.go
|
@ -1,346 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
|
||||
"github.com/gorilla/mux"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func gadmin() (*garage.APIClient, context.Context) {
|
||||
// Set Host and other parameters
|
||||
configuration := garage.NewConfiguration()
|
||||
configuration.Host = config.S3AdminEndpoint
|
||||
|
||||
// We can now generate a client
|
||||
client := garage.NewAPIClient(configuration)
|
||||
|
||||
// Authentication is handled through the context pattern
|
||||
ctx := context.WithValue(context.Background(), garage.ContextAccessToken, config.S3AdminToken)
|
||||
return client, ctx
|
||||
}
|
||||
|
||||
func grgCreateKey(name string) (*garage.KeyInfo, error) {
|
||||
client, ctx := gadmin()
|
||||
|
||||
kr := garage.AddKeyRequest{Name: &name}
|
||||
resp, _, err := client.KeyApi.AddKey(ctx).AddKeyRequest(kr).Execute()
|
||||
if err != nil {
|
||||
fmt.Printf("%+v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func grgGetKey(accessKey string) (*garage.KeyInfo, error) {
|
||||
client, ctx := gadmin()
|
||||
|
||||
resp, _, err := client.KeyApi.GetKey(ctx, accessKey).Execute()
|
||||
if err != nil {
|
||||
fmt.Printf("%+v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func grgCreateBucket(bucket string) (*garage.BucketInfo, error) {
|
||||
client, ctx := gadmin()
|
||||
|
||||
br := garage.NewCreateBucketRequest()
|
||||
br.SetGlobalAlias(bucket)
|
||||
|
||||
// Create Bucket
|
||||
binfo, _, err := client.BucketApi.CreateBucket(ctx).CreateBucketRequest(*br).Execute()
|
||||
if err != nil {
|
||||
fmt.Printf("%+v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
return binfo, nil
|
||||
}
|
||||
|
||||
func grgAllowKeyOnBucket(bid, gkey string) (*garage.BucketInfo, error) {
|
||||
client, ctx := gadmin()
|
||||
|
||||
// Allow user's key
|
||||
ar := garage.AllowBucketKeyRequest{
|
||||
BucketId: bid,
|
||||
AccessKeyId: gkey,
|
||||
Permissions: *garage.NewAllowBucketKeyRequestPermissions(true, true, true),
|
||||
}
|
||||
binfo, _, err := client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute()
|
||||
if err != nil {
|
||||
fmt.Printf("%+v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return binfo, nil
|
||||
}
|
||||
|
||||
func allowWebsiteDefault() *garage.UpdateBucketRequestWebsiteAccess {
|
||||
wr := garage.NewUpdateBucketRequestWebsiteAccess()
|
||||
wr.SetEnabled(true)
|
||||
wr.SetIndexDocument("index.html")
|
||||
wr.SetErrorDocument("error.html")
|
||||
|
||||
return wr
|
||||
}
|
||||
|
||||
func grgUpdateBucket(bid string, ur *garage.UpdateBucketRequest) (*garage.BucketInfo, error) {
|
||||
client, ctx := gadmin()
|
||||
|
||||
binfo, _, err := client.BucketApi.UpdateBucket(ctx, bid).UpdateBucketRequest(*ur).Execute()
|
||||
if err != nil {
|
||||
fmt.Printf("%+v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return updated binfo
|
||||
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 {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
|
||||
}
|
||||
|
||||
func grgDeleteBucket(bid string) error {
|
||||
client, ctx := gadmin()
|
||||
|
||||
_, 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
|
||||
}
|
||||
|
||||
tKey := getTemplate("garage_key.html")
|
||||
tKey.Execute(w, user)
|
||||
}
|
||||
|
||||
func handleWebsiteList(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
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
bucket := strings.Join(r.Form["bucket"], "")
|
||||
if bucket == "" {
|
||||
bucket = strings.Join(r.Form["bucket2"], "")
|
||||
}
|
||||
|
||||
view, err := ctrl.Create(bucket)
|
||||
if err != nil {
|
||||
tpl.Err = err
|
||||
tWebsiteNew.Execute(w, tpl)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/website/inspect/"+view.Name.Pretty, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
tWebsiteNew.Execute(w, tpl)
|
||||
}
|
||||
|
||||
type WebsiteInspectTpl struct {
|
||||
Describe *WebsiteDescribe
|
||||
View *WebsiteView
|
||||
Err error
|
||||
}
|
||||
|
||||
func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) {
|
||||
var processErr error
|
||||
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctrl, err := NewWebsiteController(user)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
if processErr == nil {
|
||||
http.Redirect(w, r, "/website", http.StatusFound)
|
||||
}
|
||||
default:
|
||||
processErr = fmt.Errorf("Unknown action")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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, &tpl)
|
||||
}
|
||||
|
||||
func handleWebsiteVhost(w http.ResponseWriter, r *http.Request) {
|
||||
var processErr error
|
||||
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctrl, err := NewWebsiteController(user)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
bucketName := mux.Vars(r)["bucket"]
|
||||
|
||||
if r.Method == "POST" {
|
||||
r.ParseForm()
|
||||
|
||||
bucket := strings.Join(r.Form["bucket"], "")
|
||||
if bucket == "" {
|
||||
bucket = strings.Join(r.Form["bucket2"], "")
|
||||
}
|
||||
|
||||
view, processErr := ctrl.Patch(bucketName, &WebsitePatch{Vhost: &bucket})
|
||||
if processErr == nil {
|
||||
http.Redirect(w, r, "/website/inspect/"+view.Name.Pretty, http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
tWebsiteEdit := getTemplate("garage_website_edit.html")
|
||||
tWebsiteEdit.Execute(w, &tpl)
|
||||
}
|
32
go.mod
32
go.mod
|
@ -1,38 +1,18 @@
|
|||
module git.deuxfleurs.fr/Deuxfleurs/guichet
|
||||
module deuxfleurs.fr/Deuxfleurs/guichet
|
||||
|
||||
go 1.18
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20230131081355-c965fe7f7dc9
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
|
||||
github.com/emersion/go-smtp v0.12.1
|
||||
github.com/go-ldap/ldap v3.0.3+incompatible
|
||||
github.com/go-ldap/ldap/v3 v3.1.6
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/jsimonetti/pwscheme v0.0.0-20220125093853-4d9895f5db73
|
||||
github.com/minio/minio-go/v7 v7.0.0
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/stretchr/objx v0.1.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6
|
||||
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 (
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1 // indirect
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/json-iterator/go v1.1.10 // indirect
|
||||
github.com/klauspost/cpuid v1.2.3 // indirect
|
||||
github.com/minio/md5-simd v1.1.0 // indirect
|
||||
github.com/minio/sha256-simd v0.1.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
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.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
|
||||
gopkg.in/ini.v1 v1.57.0 // indirect
|
||||
)
|
||||
|
|
385
go.sum
385
go.sum
|
@ -1,48 +1,4 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20230131081355-c965fe7f7dc9 h1:ERg8KCpIKym98EOKa8Gq0NSBxsasD3sqb/R0gg1wOzU=
|
||||
git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20230131081355-c965fe7f7dc9/go.mod h1:TlSL6QVxozmdRaSgP6Akspi0HCJv4HAkkq3Dldru4GM=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||
|
@ -50,68 +6,15 @@ github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rq
|
|||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||
github.com/emersion/go-smtp v0.12.1 h1:1R8BDqrR2HhlGwgFYcOi+BVTvK1bMjAB65QcVpJ5sNA=
|
||||
github.com/emersion/go-smtp v0.12.1/go.mod h1:SD9V/xa4ndMw77lR3Mf7htkp8RBNYuPh9UeuBs9tpUQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk=
|
||||
github.com/go-ldap/ldap v3.0.3+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||
github.com/go-ldap/ldap/v3 v3.1.6 h1:VTihvB7egSAvU6KOagaiA/EvgJMR2jsjRAVIho2ydBo=
|
||||
github.com/go-ldap/ldap/v3 v3.1.6/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
|
@ -119,24 +22,15 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
|
|||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jsimonetti/pwscheme v0.0.0-20220125093853-4d9895f5db73 h1:ZhC4QngptYaGx53+ph1RjxcH8fkCozBaY+935TNX4i8=
|
||||
github.com/jsimonetti/pwscheme v0.0.0-20220125093853-4d9895f5db73/go.mod h1:t0Q9JvoMTfTYdAWIk2MF69iz+Qpdk9D+PgVu6fVmaDI=
|
||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs=
|
||||
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4=
|
||||
github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
|
||||
github.com/minio/minio-go/v7 v7.0.0 h1:99hRCmsmMi+hKK93C26iPnRQebTsdK8GEx8Xb4XLr7I=
|
||||
|
@ -151,282 +45,33 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLD
|
|||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
|
||||
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 h1:D7nTwh4J0i+5mW4Zjzn5omvlr6YBcWywE6KOcatyNxY=
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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/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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
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=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
schema = 3
|
||||
|
||||
[mod]
|
||||
[mod."git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"]
|
||||
version = "v0.0.0-20230131081355-c965fe7f7dc9"
|
||||
hash = "sha256-qJN9yDMIh3xRk/3IYEWZca/biMVXXmDlPTzy0Cg11oc="
|
||||
[mod."github.com/emersion/go-sasl"]
|
||||
version = "v0.0.0-20191210011802-430746ea8b9b"
|
||||
hash = "sha256-bADpAn0ZhlTTsEB3MsG8J31cQjTtHTzohX/wkL1aMIc="
|
||||
[mod."github.com/emersion/go-smtp"]
|
||||
version = "v0.12.1"
|
||||
hash = "sha256-fiss5y7chfHv80vIQ9Xwx3J+3qLMA63EOP4OG3DxAtI="
|
||||
[mod."github.com/go-asn1-ber/asn1-ber"]
|
||||
version = "v1.3.1"
|
||||
hash = "sha256-Alh6bUq9HoBDhY+n6W7xNBto/dUMxPGvucA6guarrjc="
|
||||
[mod."github.com/go-ldap/ldap/v3"]
|
||||
version = "v3.1.6"
|
||||
hash = "sha256-UPUdYKOoCQWgl2Onbq1Oql7XU4TeYQA/+j4atwhdKbE="
|
||||
[mod."github.com/golang/protobuf"]
|
||||
version = "v1.4.2"
|
||||
hash = "sha256-zhA1d1Kw1ZV/kDBZ4Iv5miKHjZBhcV8m3BiD1qocJqw="
|
||||
[mod."github.com/google/uuid"]
|
||||
version = "v1.1.1"
|
||||
hash = "sha256-66PXC/RCPUyhS9PhkIPQFR3tbM2zZYDNPGXN7JJj3UE="
|
||||
[mod."github.com/gorilla/mux"]
|
||||
version = "v1.7.3"
|
||||
hash = "sha256-YZSIN7Ua+hPqSIrT+tiRz3aFqJ1EWHvwee+PptpHI28="
|
||||
[mod."github.com/gorilla/securecookie"]
|
||||
version = "v1.1.1"
|
||||
hash = "sha256-IBBYWfdOuXvQsb01DaA8tBizCfAE1J2KLXIn3W+NeJk="
|
||||
[mod."github.com/gorilla/sessions"]
|
||||
version = "v1.2.0"
|
||||
hash = "sha256-4V7yd/vf03CEsb3pz5dbLWwv7t9QgKkEhVXtc1/z5s8="
|
||||
[mod."github.com/jsimonetti/pwscheme"]
|
||||
version = "v0.0.0-20220125093853-4d9895f5db73"
|
||||
hash = "sha256-YF3RKU/4CWvLPgGzUd7Hk/2+41OUFuRWZgzQuqcsKg0="
|
||||
[mod."github.com/json-iterator/go"]
|
||||
version = "v1.1.10"
|
||||
hash = "sha256-jdS2C0WsgsWREBSj+YUzSqdZofMfUMecaOQ/lB9Mu6k="
|
||||
[mod."github.com/klauspost/cpuid"]
|
||||
version = "v1.2.3"
|
||||
hash = "sha256-1IBlONMxKVgudV/mzNrFZB60z8w4xFjVbEU2DoIAoeg="
|
||||
[mod."github.com/minio/md5-simd"]
|
||||
version = "v1.1.0"
|
||||
hash = "sha256-jJbDwg7KlLB991wj1U6y+kJKOUxKVGQrDbM3nY+6qxE="
|
||||
[mod."github.com/minio/minio-go/v7"]
|
||||
version = "v7.0.0"
|
||||
hash = "sha256-xWAELgH6mWVGKFEe2gbzvigJDNk+ELmegJe09KvUqvY="
|
||||
[mod."github.com/minio/sha256-simd"]
|
||||
version = "v0.1.1"
|
||||
hash = "sha256-HpcuLTnpcyKe0ua2MN/ysK5cXdrwquDjrx4Y2dG6W2s="
|
||||
[mod."github.com/mitchellh/go-homedir"]
|
||||
version = "v1.1.0"
|
||||
hash = "sha256-oduBKXHAQG8X6aqLEpqZHs5DOKe84u6WkBwi4W6cv3k="
|
||||
[mod."github.com/modern-go/concurrent"]
|
||||
version = "v0.0.0-20180228061459-e0a39a4cb421"
|
||||
hash = "sha256-+bdeHUArnpkk4eMQIwXm9K249NkpwAjoTrXrGn/4VUE="
|
||||
[mod."github.com/modern-go/reflect2"]
|
||||
version = "v0.0.0-20180701023420-4b7aa43c6742"
|
||||
hash = "sha256-RyIwgrPwtd4lNjLGkBVxRvu5IdXLDqf5F69QWLm0zLw="
|
||||
[mod."github.com/nfnt/resize"]
|
||||
version = "v0.0.0-20180221191011-83c6a9932646"
|
||||
hash = "sha256-yvPV+HlDOyJsiwAcVHQkmtw8DHSXyw+cXHkigXm8rAA="
|
||||
[mod."golang.org/x/crypto"]
|
||||
version = "v0.0.0-20200622213623-75b288015ac9"
|
||||
hash = "sha256-QvFbJEm3gXs2NtaaREbkbAtdHpU4fqX+0C0EvTezdKM="
|
||||
[mod."golang.org/x/net"]
|
||||
version = "v0.0.0-20200822124328-c89045814202"
|
||||
hash = "sha256-wg5IrlVfnsRL86dbi3WJ9XA6Er6JuuyusytIPf18mO0="
|
||||
[mod."golang.org/x/oauth2"]
|
||||
version = "v0.0.0-20210323180902-22b0adad7558"
|
||||
hash = "sha256-mQv+EELtNg99ZYiRFxel405A66PtHK6eCx6XM3vqKG8="
|
||||
[mod."golang.org/x/sys"]
|
||||
version = "v0.12.0"
|
||||
hash = "sha256-Ht/PhBJGWNBg4ksmdUu4+7hJjFypSwoUN/8DJricd+0="
|
||||
[mod."golang.org/x/term"]
|
||||
version = "v0.12.0"
|
||||
hash = "sha256-NFko0uqv/r2VxrbSgS1IwWzaWQK3RZuk0MvUV+qxIsc="
|
||||
[mod."golang.org/x/text"]
|
||||
version = "v0.3.3"
|
||||
hash = "sha256-kiauoT7vd7Mh2AW7TnceQyoCDsARxWkDZu1OSD9dCZw="
|
||||
[mod."google.golang.org/appengine"]
|
||||
version = "v1.6.6"
|
||||
hash = "sha256-nZnEfsXy3mgzRnlyWGHJKqsosvnAQFkhVszw3DSFe6Y="
|
||||
[mod."google.golang.org/protobuf"]
|
||||
version = "v1.25.0"
|
||||
hash = "sha256-3sf57K5A0nmA1UmDe+6FUNJI6UR+SfVyZWNv+2TGHT4="
|
||||
[mod."gopkg.in/ini.v1"]
|
||||
version = "v1.57.0"
|
||||
hash = "sha256-WSjX+qHJ1Rf4FRMTs7udQwEBkIo+z8+EK3uB5CebrZ4="
|
|
@ -1,16 +0,0 @@
|
|||
# Intégration de Guichet dans un environnement de dev/test
|
||||
|
||||
## Dev process
|
||||
|
||||
On utilise `docker compose` pour mettre en place l'infrastructure dont dépend Guichet, que l'on développe. (On rajoutera Garage dedans plus tard.)
|
||||
|
||||
On ne met pas Guichet dans le `compose` pour pouvoir itérer plus rapidement : un `go build` et on a la nouvelle version, sans avoir restart les dépendances (Bottin, Consul...).
|
||||
|
||||
## Notes
|
||||
|
||||
* Bien récupérer le password `admin` dans les logs de 1er lancement de Bottin : il ne sera pas réaffiché.
|
||||
* Identifiant de l'admin sur Guichet : `cn=admin,dc=bottin,dc=eu` because il n'est pas dans `ou=users,dc=bottin,dc=eu` qui est l'organisation par défaut dans laquelle on va chercher les utilisateurs.
|
||||
|
||||
## TODO
|
||||
|
||||
* Bridger Garage/S3 (pour le moment ne sert que pour les avatars dans l'annuaire)
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"suffix": "dc=bottin,dc=eu",
|
||||
"bind": "bottin:389",
|
||||
"consul_host": "consul:8500",
|
||||
"acl": [
|
||||
"ANONYMOUS::bind:*,ou=users,dc=bottin,dc=eu:",
|
||||
"ANONYMOUS::bind:cn=admin,dc=bottin,dc=eu:",
|
||||
"*,dc=bottin,dc=eu::read:*:* !userpassword",
|
||||
"cn=admin,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,26 +0,0 @@
|
|||
metadata_dir = "/tmp/meta"
|
||||
data_dir = "/tmp/data"
|
||||
db_engine = "lmdb"
|
||||
|
||||
replication_mode = "none"
|
||||
|
||||
rpc_bind_addr = "[::]:3901"
|
||||
rpc_public_addr = "127.0.0.1:3901"
|
||||
rpc_secret = "93086c2378eecea1cc9e83ee0554a8c510359215168774a396dcb5a01f88dd79"
|
||||
|
||||
[s3_api]
|
||||
s3_region = "garage"
|
||||
api_bind_addr = "[::]:3900"
|
||||
root_domain = ".s3.garage.localhost"
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = ".web.garage.localhost"
|
||||
index = "index.html"
|
||||
|
||||
[k2v_api]
|
||||
api_bind_addr = "[::]:3904"
|
||||
|
||||
[admin]
|
||||
api_bind_addr = "0.0.0.0:3903"
|
||||
admin_token = "GlXP43PWH3LuvEGSNxKYzZCyUss8VqZmarBU+HUlrxw="
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"http_bind_addr": ":9991",
|
||||
"ldap_server_addr": "ldap://127.0.0.1:389",
|
||||
|
||||
"base_dn": "dc=bottin,dc=eu",
|
||||
"user_base_dn": "ou=users,dc=bottin,dc=eu",
|
||||
"user_name_attr": "cn",
|
||||
"group_base_dn": "ou=groups,dc=bottin,dc=eu",
|
||||
"group_name_attr": "cn",
|
||||
|
||||
"invitation_base_dn": "ou=invitations,dc=bottin,dc=eu",
|
||||
"invitation_name_attr": "cn",
|
||||
"invited_mail_format": "{}@bottin.eu",
|
||||
"invited_auto_groups": [
|
||||
"cn=email,ou=groups,dc=bottin,dc=eu"
|
||||
],
|
||||
|
||||
"web_address": "https://guichet.bottin.eu",
|
||||
"mail_from": "welcome@bottin.eu",
|
||||
"smtp_server": "smtp.bottin.eu",
|
||||
"smtp_username": "guichet",
|
||||
"smtp_password": "",
|
||||
|
||||
"admin_account": "cn=admin,dc=bottin,dc=eu",
|
||||
"group_can_admin": "gid=admin,ou=groups,dc=bottin,dc=eu",
|
||||
"group_can_invite": "",
|
||||
|
||||
"s3_endpoint": "garage.bottin.eu",
|
||||
"s3_access_key": "",
|
||||
"s3_secret_key": "",
|
||||
"s3_region": "garage",
|
||||
"s3_bucket": "bottin-pictures"
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
version: '3'
|
||||
services:
|
||||
consul:
|
||||
image: hashicorp/consul:1.16
|
||||
restart: "always"
|
||||
expose:
|
||||
- 8500
|
||||
bottin:
|
||||
image: dxflrs/bottin:7h18i30cckckaahv87d3c86pn4a7q41z
|
||||
#command: "-config /etc/bottin.json"
|
||||
restart: "always"
|
||||
depends_on: ["consul"]
|
||||
ports:
|
||||
- "389:389"
|
||||
volumes:
|
||||
- "./config/bottin.json:/config.json"
|
||||
garage:
|
||||
image: dxflrs/garage:v0.8.2
|
||||
ports:
|
||||
- "3900:3900"
|
||||
- "3902:3902"
|
||||
- "3903:3903"
|
||||
- "3904:3904"
|
||||
volumes:
|
||||
- "./config/garage.toml:/etc/garage.toml"
|
74
invite.go
74
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) *LoggedUser {
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
func checkInviterLogin(w http.ResponseWriter, r *http.Request) *LoginStatus {
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !user.Capabilities.CanInvite {
|
||||
if !login.CanInvite {
|
||||
http.Error(w, "Not authorized to invite new users.", http.StatusUnauthorized)
|
||||
return nil
|
||||
}
|
||||
|
||||
return user
|
||||
return login
|
||||
}
|
||||
|
||||
// New account creation directly from interface
|
||||
|
||||
func handleInviteNewAccount(w http.ResponseWriter, r *http.Request) {
|
||||
user := checkInviterLogin(w, r)
|
||||
if user == nil {
|
||||
login := checkInviterLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
handleNewAccount(w, r, user.Login.conn, user.Login.Info.DN())
|
||||
handleNewAccount(w, r, login.conn, login.Info.DN)
|
||||
}
|
||||
|
||||
// New account creation using code
|
||||
|
@ -52,16 +52,15 @@ func handleInvitationCode(w http.ResponseWriter, r *http.Request) {
|
|||
code := mux.Vars(r)["code"]
|
||||
code_id, code_pw := readCode(code)
|
||||
|
||||
l, err := NewLdapCon()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
l := ldapOpen(w)
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
|
||||
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 := template.Must(template.ParseFiles("templates/layout.html", "templates/invite_invalid_code.html"))
|
||||
templateInviteInvalidCode.Execute(w, nil)
|
||||
return
|
||||
}
|
||||
|
@ -111,7 +110,7 @@ type NewAccountData struct {
|
|||
}
|
||||
|
||||
func handleNewAccount(w http.ResponseWriter, r *http.Request, l *ldap.Conn, invitedBy string) bool {
|
||||
templateInviteNewAccount := getTemplate("invite_new_account.html")
|
||||
templateInviteNewAccount := template.Must(template.ParseFiles("templates/layout.html", "templates/invite_new_account.html"))
|
||||
|
||||
data := &NewAccountData{}
|
||||
|
||||
|
@ -134,12 +133,9 @@ func handleNewAccount(w http.ResponseWriter, r *http.Request, l *ldap.Conn, invi
|
|||
}
|
||||
|
||||
func tryCreateAccount(l *ldap.Conn, data *NewAccountData, pass1 string, pass2 string, invitedBy string) {
|
||||
checkFailed := false
|
||||
|
||||
// Check if username is correct
|
||||
if match, err := regexp.MatchString("^[a-z0-9._-]+$", data.Username); !(err == nil && match) {
|
||||
if match, err := regexp.MatchString("^[a-zA-Z0-9._-]+$", data.Username); !(err == nil && match) {
|
||||
data.ErrorInvalidUsername = true
|
||||
checkFailed = true
|
||||
}
|
||||
|
||||
// Check if user exists
|
||||
|
@ -154,26 +150,22 @@ func tryCreateAccount(l *ldap.Conn, data *NewAccountData, pass1 string, pass2 st
|
|||
sr, err := l.Search(searchRq)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
checkFailed = true
|
||||
return
|
||||
}
|
||||
|
||||
if len(sr.Entries) > 0 {
|
||||
data.ErrorUsernameTaken = true
|
||||
checkFailed = true
|
||||
return
|
||||
}
|
||||
|
||||
// Check that password is long enough
|
||||
if len(pass1) < 8 {
|
||||
data.ErrorPasswordTooShort = true
|
||||
checkFailed = true
|
||||
return
|
||||
}
|
||||
|
||||
if pass1 != pass2 {
|
||||
data.ErrorPasswordMismatch = true
|
||||
checkFailed = true
|
||||
}
|
||||
|
||||
if checkFailed {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -181,12 +173,7 @@ func tryCreateAccount(l *ldap.Conn, data *NewAccountData, pass1 string, pass2 st
|
|||
req := ldap.NewAddRequest(userDn, nil)
|
||||
req.Attribute("objectclass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"})
|
||||
req.Attribute("structuralobjectclass", []string{"inetOrgPerson"})
|
||||
pw, err := SSHAEncode(pass1)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
return
|
||||
}
|
||||
req.Attribute("userpassword", []string{pw})
|
||||
req.Attribute("userpassword", []string{SSHAEncode([]byte(pass1))})
|
||||
req.Attribute("invitedby", []string{invitedBy})
|
||||
if len(data.DisplayName) > 0 {
|
||||
req.Attribute("displayname", []string{data.DisplayName})
|
||||
|
@ -240,10 +227,10 @@ type CodeMailFields struct {
|
|||
}
|
||||
|
||||
func handleInviteSendCode(w http.ResponseWriter, r *http.Request) {
|
||||
templateInviteSendCode := getTemplate("invite_send_code.html")
|
||||
templateInviteSendCode := template.Must(template.ParseFiles("templates/layout.html", "templates/invite_send_code.html"))
|
||||
|
||||
user := checkInviterLogin(w, r)
|
||||
if user == nil {
|
||||
login := checkInviterLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -258,29 +245,24 @@ func handleInviteSendCode(w http.ResponseWriter, r *http.Request) {
|
|||
sendto := strings.Join(r.Form["sendto"], "")
|
||||
|
||||
if choice == "display" || choice == "send" {
|
||||
trySendCode(user, choice, sendto, data)
|
||||
trySendCode(login, choice, sendto, data)
|
||||
}
|
||||
}
|
||||
|
||||
templateInviteSendCode.Execute(w, data)
|
||||
}
|
||||
|
||||
func trySendCode(user *LoggedUser, choice string, sendto string, data *SendCodeData) {
|
||||
func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCodeData) {
|
||||
// Generate code
|
||||
code, code_id, code_pw := genCode()
|
||||
|
||||
// Create invitation object in database
|
||||
inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN
|
||||
req := ldap.NewAddRequest(inviteDn, nil)
|
||||
pw, err := SSHAEncode(code_pw)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
return
|
||||
}
|
||||
req.Attribute("userpassword", []string{pw})
|
||||
req.Attribute("userpassword", []string{SSHAEncode([]byte(code_pw))})
|
||||
req.Attribute("objectclass", []string{"top", "invitationCode"})
|
||||
|
||||
err = user.Login.conn.Add(req)
|
||||
err := login.conn.Add(req)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
return
|
||||
|
@ -299,12 +281,12 @@ func trySendCode(user *LoggedUser, choice string, sendto string, data *SendCodeD
|
|||
return
|
||||
}
|
||||
|
||||
templateMail := template.Must(template.ParseFiles(templatePath + "/invite_mail.txt"))
|
||||
templateMail := template.Must(template.ParseFiles("templates/invite_mail.txt"))
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
templateMail.Execute(buf, &CodeMailFields{
|
||||
To: sendto,
|
||||
From: config.MailFrom,
|
||||
InviteFrom: user.WelcomeName(),
|
||||
InviteFrom: login.WelcomeName(),
|
||||
Code: code,
|
||||
WebBaseAddress: config.WebAddress,
|
||||
})
|
||||
|
|
294
login.go
294
login.go
|
@ -1,294 +0,0 @@
|
|||
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
|
||||
}
|
250
main.go
250
main.go
|
@ -2,8 +2,10 @@ package main
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
@ -27,10 +29,6 @@ type ConfigFile struct {
|
|||
GroupBaseDN string `json:"group_base_dn"`
|
||||
GroupNameAttr string `json:"group_name_attr"`
|
||||
|
||||
MailingBaseDN string `json:"mailing_list_base_dn"`
|
||||
MailingNameAttr string `json:"mailing_list_name_attr"`
|
||||
MailingGuestsBaseDN string `json:"mailing_list_guest_user_base_dn"`
|
||||
|
||||
InvitationBaseDN string `json:"invitation_base_dn"`
|
||||
InvitationNameAttr string `json:"invitation_name_attr"`
|
||||
InvitedMailFormat string `json:"invited_mail_format"`
|
||||
|
@ -46,9 +44,6 @@ type ConfigFile struct {
|
|||
GroupCanInvite string `json:"group_can_invite"`
|
||||
GroupCanAdmin string `json:"group_can_admin"`
|
||||
|
||||
S3AdminEndpoint string `json:"s3_admin_endpoint"`
|
||||
S3AdminToken string `json:"s3_admin_token"`
|
||||
|
||||
S3Endpoint string `json:"s3_endpoint"`
|
||||
S3AccessKey string `json:"s3_access_key"`
|
||||
S3SecretKey string `json:"s3_secret_key"`
|
||||
|
@ -56,16 +51,12 @@ type ConfigFile struct {
|
|||
S3Bucket string `json:"s3_bucket"`
|
||||
}
|
||||
|
||||
var fsServer = flag.NewFlagSet("server", flag.ContinueOnError)
|
||||
var configFlag = fsServer.String("config", "./config.json", "Configuration file path")
|
||||
var configFlag = flag.String("config", "./config.json", "Configuration file path")
|
||||
|
||||
var config *ConfigFile
|
||||
|
||||
const SESSION_NAME = "guichet_session"
|
||||
|
||||
var staticPath = "./static"
|
||||
var templatePath = "./templates"
|
||||
|
||||
var store sessions.Store = nil
|
||||
|
||||
func readConfig() ConfigFile {
|
||||
|
@ -103,35 +94,9 @@ func readConfig() ConfigFile {
|
|||
return config_file
|
||||
}
|
||||
|
||||
func getTemplate(name string) *template.Template {
|
||||
return template.Must(template.New("layout.html").Funcs(template.FuncMap{
|
||||
"contains": strings.Contains,
|
||||
}).ParseFiles(
|
||||
templatePath+"/layout.html",
|
||||
templatePath+"/"+name,
|
||||
))
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
server(os.Args[1:])
|
||||
return
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
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
|
||||
|
||||
|
@ -144,12 +109,8 @@ func server(args []string) {
|
|||
|
||||
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)
|
||||
|
@ -157,24 +118,16 @@ func server(args []string) {
|
|||
r.HandleFunc("/directory/search", handleDirectorySearch)
|
||||
r.HandleFunc("/directory", handleDirectory)
|
||||
|
||||
r.HandleFunc("/website", handleWebsiteList)
|
||||
r.HandleFunc("/website/new", handleWebsiteNew)
|
||||
r.HandleFunc("/website/configure", handleWebsiteConfigure)
|
||||
r.HandleFunc("/website/inspect/{bucket}", handleWebsiteInspect)
|
||||
r.HandleFunc("/website/vhost/{bucket}", handleWebsiteVhost)
|
||||
|
||||
r.HandleFunc("/invite/new_account", handleInviteNewAccount)
|
||||
r.HandleFunc("/invite/send_code", handleInviteSendCode)
|
||||
r.HandleFunc("/invitation/{code}", handleInvitationCode)
|
||||
|
||||
r.HandleFunc("/admin/users", handleAdminUsers)
|
||||
r.HandleFunc("/admin/groups", handleAdminGroups)
|
||||
r.HandleFunc("/admin/mailing", handleAdminMailing)
|
||||
r.HandleFunc("/admin/mailing/{id}", handleAdminMailingList)
|
||||
r.HandleFunc("/admin/ldap/{dn}", handleAdminLDAP)
|
||||
r.HandleFunc("/admin/create/{template}/{super_dn}", handleAdminCreate)
|
||||
|
||||
staticfiles := http.FileServer(http.Dir(staticPath))
|
||||
staticfiles := http.FileServer(http.Dir("static"))
|
||||
r.Handle("/static/{file:.*}", http.StripPrefix("/static/", staticfiles))
|
||||
|
||||
log.Printf("Starting HTTP server on %s", config.HttpBindAddr)
|
||||
|
@ -184,6 +137,31 @@ 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 {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL)
|
||||
|
@ -191,31 +169,148 @@ 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",
|
||||
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 {
|
||||
User *LoggedUser
|
||||
Login *LoginStatus
|
||||
BaseDN string
|
||||
}
|
||||
|
||||
func handleHome(w http.ResponseWriter, r *http.Request) {
|
||||
templateHome := getTemplate("home.html")
|
||||
templateHome := template.Must(template.ParseFiles("templates/layout.html", "templates/home.html"))
|
||||
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := &HomePageData{
|
||||
User: user,
|
||||
Login: login,
|
||||
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 {
|
||||
|
@ -232,10 +327,9 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
// --- Login Controller ---
|
||||
type LoginFormData struct {
|
||||
Username string
|
||||
WrongUser bool
|
||||
|
@ -243,26 +337,28 @@ type LoginFormData struct {
|
|||
ErrorMessage string
|
||||
}
|
||||
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
templateLogin := getTemplate("login.html")
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo {
|
||||
templateLogin := template.Must(template.ParseFiles("templates/layout.html", "templates/login.html"))
|
||||
|
||||
if r.Method == "GET" {
|
||||
templateLogin.Execute(w, LoginFormData{})
|
||||
return
|
||||
return nil
|
||||
} else if r.Method == "POST" {
|
||||
r.ParseForm()
|
||||
|
||||
username := strings.Join(r.Form["username"], "")
|
||||
password := strings.Join(r.Form["password"], "")
|
||||
loginInfo := LoginInfo{username, password}
|
||||
|
||||
l, err := NewLdapCon()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
user_dn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, username, config.UserBaseDN)
|
||||
if strings.EqualFold(username, config.AdminAccount) {
|
||||
user_dn = username
|
||||
}
|
||||
|
||||
err = l.Bind(loginInfo.DN(), loginInfo.Password)
|
||||
l := ldapOpen(w)
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := l.Bind(user_dn, password)
|
||||
if err != nil {
|
||||
data := &LoginFormData{
|
||||
Username: username,
|
||||
|
@ -275,7 +371,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||
data.ErrorMessage = err.Error()
|
||||
}
|
||||
templateLogin.Execute(w, data)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Successfully logged in, save it to session
|
||||
|
@ -286,15 +382,21 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
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
|
||||
return nil
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return &LoginInfo{
|
||||
DN: user_dn,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "Unsupported method", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
12
picture.go
12
picture.go
|
@ -43,8 +43,8 @@ func newMinioClient() (*minio.Client, error) {
|
|||
return minioCLient, nil
|
||||
}
|
||||
|
||||
// Upload image through guichet server.
|
||||
func uploadProfilePicture(w http.ResponseWriter, r *http.Request, user *LoggedUser) (string, error) {
|
||||
//Upload image through guichet server.
|
||||
func uploadProfilePicture(w http.ResponseWriter, r *http.Request, login *LoginStatus) (string, error) {
|
||||
file, _, err := r.FormFile("image")
|
||||
|
||||
if err == http.ErrMissingFile {
|
||||
|
@ -74,7 +74,7 @@ func uploadProfilePicture(w http.ResponseWriter, r *http.Request, user *LoggedUs
|
|||
|
||||
// If a previous profile picture existed, delete it
|
||||
// (don't care about errors)
|
||||
if nameConsul := user.Entry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE); nameConsul != "" {
|
||||
if nameConsul := login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE); nameConsul != "" {
|
||||
mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul, minio.RemoveObjectOptions{})
|
||||
mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul+"-thumb", minio.RemoveObjectOptions{})
|
||||
}
|
||||
|
@ -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"]
|
||||
|
||||
// Get user
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
//Check login
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
58
profile.go
58
profile.go
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
|
@ -8,7 +9,7 @@ import (
|
|||
)
|
||||
|
||||
type ProfileTplData struct {
|
||||
User *LoggedUser
|
||||
Status *LoginStatus
|
||||
ErrorMessage string
|
||||
Success bool
|
||||
Mail string
|
||||
|
@ -21,26 +22,26 @@ type ProfileTplData struct {
|
|||
}
|
||||
|
||||
func handleProfile(w http.ResponseWriter, r *http.Request) {
|
||||
templateProfile := getTemplate("profile.html")
|
||||
templateProfile := template.Must(template.ParseFiles("templates/layout.html", "templates/profile.html"))
|
||||
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := &ProfileTplData{
|
||||
User: user,
|
||||
Status: login,
|
||||
ErrorMessage: "",
|
||||
Success: false,
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
if r.Method == "POST" {
|
||||
//5MB maximum size files
|
||||
|
@ -56,7 +57,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
data.Visibility = visible
|
||||
|
||||
profilePicture, err := uploadProfilePicture(w, r, user)
|
||||
profilePicture, err := uploadProfilePicture(w, r, login)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
}
|
||||
|
@ -65,7 +66,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
|
|||
data.ProfilePicture = profilePicture
|
||||
}
|
||||
|
||||
modify_request := ldap.NewModifyRequest(user.Login.Info.DN(), nil)
|
||||
modify_request := ldap.NewModifyRequest(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 +76,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
|
|||
modify_request.Replace(FIELD_NAME_PROFILE_PICTURE, []string{data.ProfilePicture})
|
||||
}
|
||||
|
||||
err = user.Login.conn.Modify(modify_request)
|
||||
err = login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
} else {
|
||||
|
@ -88,7 +89,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
type PasswdTplData struct {
|
||||
User *LoggedUser
|
||||
Status *LoginStatus
|
||||
ErrorMessage string
|
||||
TooShortError bool
|
||||
NoMatchError bool
|
||||
|
@ -96,15 +97,15 @@ type PasswdTplData struct {
|
|||
}
|
||||
|
||||
func handlePasswd(w http.ResponseWriter, r *http.Request) {
|
||||
templatePasswd := getTemplate("passwd.html")
|
||||
templatePasswd := template.Must(template.ParseFiles("templates/layout.html", "templates/passwd.html"))
|
||||
|
||||
user := RequireUserHtml(w, r)
|
||||
if user == nil {
|
||||
login := checkLogin(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := &PasswdTplData{
|
||||
User: user,
|
||||
Status: login,
|
||||
ErrorMessage: "",
|
||||
Success: false,
|
||||
}
|
||||
|
@ -120,18 +121,13 @@ func handlePasswd(w http.ResponseWriter, r *http.Request) {
|
|||
} else if password2 != password {
|
||||
data.NoMatchError = true
|
||||
} else {
|
||||
modify_request := ldap.NewModifyRequest(user.Login.Info.DN(), nil)
|
||||
pw, err := SSHAEncode(password)
|
||||
if err == nil {
|
||||
modify_request.Replace("userpassword", []string{pw})
|
||||
err := user.Login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
} else {
|
||||
data.Success = true
|
||||
}
|
||||
} else {
|
||||
modify_request := ldap.NewModifyRequest(login.Info.DN, nil)
|
||||
modify_request.Replace("userpassword", []string{SSHAEncode([]byte(password))})
|
||||
err := login.conn.Modify(modify_request)
|
||||
if err != nil {
|
||||
data.ErrorMessage = err.Error()
|
||||
} else {
|
||||
data.Success = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
151
quotas.go
151
quotas.go
|
@ -1,151 +0,0 @@
|
|||
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)
|
||||
}
|
33
ssha.go
33
ssha.go
|
@ -1,10 +1,37 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/jsimonetti/pwscheme/ssha512"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Encode encodes the []byte of raw password
|
||||
func SSHAEncode(rawPassPhrase string) (string, error) {
|
||||
return ssha512.Generate(rawPassPhrase, 16)
|
||||
func SSHAEncode(rawPassPhrase []byte) string {
|
||||
hash := makeSSHAHash(rawPassPhrase, makeSalt())
|
||||
b64 := base64.StdEncoding.EncodeToString(hash)
|
||||
return fmt.Sprintf("{ssha}%s", b64)
|
||||
}
|
||||
|
||||
// makeSalt make a 32 byte array containing random bytes.
|
||||
func makeSalt() []byte {
|
||||
sbytes := make([]byte, 32)
|
||||
_, err := rand.Read(sbytes)
|
||||
if err != nil {
|
||||
log.Panicf("Could not read random bytes: %s", err)
|
||||
}
|
||||
return sbytes
|
||||
}
|
||||
|
||||
// makeSSHAHash make hasing using SHA-1 with salt. This is not the final output though. You need to append {SSHA} string with base64 of this hash.
|
||||
func makeSSHAHash(passphrase, salt []byte) []byte {
|
||||
sha := sha1.New()
|
||||
sha.Write(passphrase)
|
||||
sha.Write(salt)
|
||||
|
||||
h := sha.Sum(nil)
|
||||
return append(h, salt...)
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -30,40 +30,33 @@
|
|||
<input type="text" disabled="true" class="form-control" value="{{ .SuperDN }}" />
|
||||
</div>
|
||||
-->
|
||||
{{if eq .Template "ml"}}
|
||||
<div class="form-group">
|
||||
<label for="idvalue">Adresse complète de la mailing list :</label>
|
||||
<input type="text" id="idvalue" name="idvalue" class="form-control" value="{{ .IdValue }}" placeholder="example@deuxfleurs.fr" />
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="form-group">
|
||||
<label for="idvalue">Identifiant :</label>
|
||||
<input type="text" id="idvalue" name="idvalue" class="form-control" value="{{ .IdValue }}" />
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-group">
|
||||
<label for="idtype">Type d'identifiant :</label>
|
||||
<label for="idvalue">Identifiant:</label>
|
||||
<input type="text" id="idvalue" name="idvalue" class="form-control" value="{{ .IdValue }}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="idtype">Type d'identifiant:</label>
|
||||
<input type="text" {{if .Template}}disabled="disabled"{{end}} id="idtype" name="idtype" class="form-control" value="{{ .IdType }}" />
|
||||
</div>
|
||||
{{ if eq .Template "user" }}
|
||||
<div class="form-group">
|
||||
<label for="displayname">Nom :</label>
|
||||
<label for="displayname">Nom:</label>
|
||||
<input type="text" id="displayname" name="displayname" class="form-control" value="{{ .DisplayName }}" />
|
||||
</div>
|
||||
<input type="hidden" name="description" value="" />
|
||||
{{ else }}
|
||||
<div class="form-group">
|
||||
<label for="description">Description :</label>
|
||||
<label for="description">Description:</label>
|
||||
<input type="text" id="description" name="description" class="form-control" value="{{ .Description }}" />
|
||||
</div>
|
||||
<input type="hidden" name="displayname" value="" />
|
||||
{{ end }}
|
||||
<div class="form-group">
|
||||
<label for="soc">StructuralObjectClass :</label>
|
||||
<label for="soc">StructuralObjectClass:</label>
|
||||
<input type="text" {{if .Template}}disabled="disabled"{{end}} id="soc" name="soc" class="form-control" value="{{ .StructuralObjectClass }}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oc">ObjectClass :</label>
|
||||
<label for="oc">ObjectClass:</label>
|
||||
<textarea rows="5" {{if .Template}}disabled="disabled"{{end}} id="oc" name="oc" class="form-control">{{ .ObjectClass }}</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Créer l'objet</button>
|
||||
|
|
|
@ -8,11 +8,6 @@
|
|||
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning mt-4">
|
||||
Les groupes servent uniquement à contrôler l'accès à différentes fonctionalités de Deuxfleurs.
|
||||
Ce ne sont pas des <a href="/admin/mailing">mailing lists</a>.
|
||||
</div>
|
||||
|
||||
<table class="table mt-4">
|
||||
<thead>
|
||||
<th scope="col">Identifiant</th>
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
{{define "body"}}
|
||||
|
||||
<div class="d-flex">
|
||||
<h4>Explorateur LDAP</h4>
|
||||
<a class="ml-auto btn btn-info" href="/">Menu principal</a>
|
||||
<h4>LDAP-Explorer</h4>
|
||||
<a class="ml-auto btn btn-info" href="/">Main Menu</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
|
@ -23,17 +23,7 @@
|
|||
|
||||
<table class="table mt-4">
|
||||
<tbody>
|
||||
{{range .ChildrenOU}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/admin/ldap/{{.DN}}">
|
||||
🗀 {{.Identifier}}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{.Name}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{range .ChildrenOther}}
|
||||
{{range .Children}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/admin/ldap/{{.DN}}">
|
||||
|
@ -48,25 +38,25 @@
|
|||
|
||||
{{if .CanAddChild}}
|
||||
<div class="mt-2">
|
||||
<a class="btn btn-sm btn-success" href="/admin/create/user/{{.DN}}">+utilisateur</a>
|
||||
<a class="ml-4 btn btn-sm btn-success" href="/admin/create/group/{{.DN}}">+groupe</a>
|
||||
<a class="btn btn-sm btn-success" href="/admin/create/user/{{.DN}}">+user</a>
|
||||
<a class="ml-4 btn btn-sm btn-success" href="/admin/create/group/{{.DN}}">+group</a>
|
||||
<a class="ml-4 btn btn-sm btn-success" href="/admin/create/ou/{{.DN}}">+ou</a>
|
||||
<a class="ml-4 btn btn-sm btn-success" href="/admin/create/generic/{{.DN}}">+objet</a>
|
||||
<a class="ml-4 btn btn-sm btn-success" href="/admin/create/generic/{{.DN}}">+object</a>
|
||||
</div>
|
||||
<hr class="mt-4" />
|
||||
{{end}}
|
||||
|
||||
{{if .Success}}
|
||||
<div class="alert alert-success mt-2">Modification enregistrée.</div>
|
||||
<div class="alert alert-success mt-2">Modifications have been saved.</div>
|
||||
{{end}}
|
||||
{{if .Error}}
|
||||
<div class="alert alert-danger mt-2">
|
||||
Impossible d'effectuer la modification.
|
||||
Impossible to execute the requested change.
|
||||
<div style="font-size: 0.8em">{{.Error}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<h5>Attributs</h5>
|
||||
<h5>Attributes</h5>
|
||||
<div class="container">
|
||||
{{range $key, $value := .Props}}
|
||||
{{if $value.Editable}}
|
||||
|
@ -88,7 +78,7 @@
|
|||
|
||||
<div class="col-md-1">
|
||||
{{if $value.Deletable}}
|
||||
<form method="POST" onsubmit="return confirm('Supprimer cet attribut ?');">
|
||||
<form method="POST" onsubmit="return confirm('Do you really want to delete this attribute ?');">
|
||||
<input type="hidden" name="action" value="delete" />
|
||||
<input type="hidden" name="attr" value="{{$key}}" />
|
||||
<input type="submit" value="Suppr." class="form-control btn btn-danger btn-sm" />
|
||||
|
@ -104,11 +94,7 @@
|
|||
<div class="col-md-3"><strong>{{$key}}</strong></div>
|
||||
<div class="col-md-9">
|
||||
{{range $value.Values}}
|
||||
{{if eq $key "creatorsname" "modifiersname" }}
|
||||
<div><a href="/admin/ldap/{{.}}">{{.}}</a></div>
|
||||
{{else}}
|
||||
<div>{{.}}</div>
|
||||
{{end}}
|
||||
<div>{{.}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -118,11 +104,11 @@
|
|||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
<input type="hidden" name="action" value="add" />
|
||||
<input class="form-control" type="text" name="attr" placeholder="Ajouter un attribut..." />
|
||||
<input class="form-control" type="text" name="attr" placeholder="Add an attribute..." />
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="form-row">
|
||||
<textarea name="values" placeholder="Valeur(s)..." rows="2" class="form-control col-md-9"></textarea>
|
||||
<textarea name="values" placeholder="Value(s)..." rows="2" class="form-control col-md-9"></textarea>
|
||||
<div class="col-md-3">
|
||||
<input type="submit" value="Ajouter" class="form-control btn btn-success" />
|
||||
</div>
|
||||
|
@ -134,7 +120,7 @@
|
|||
|
||||
{{if .HasMembers}}
|
||||
<hr class="mt-4" />
|
||||
<h5 class="mt-4">Membres</h5>
|
||||
<h5 class="mt-4">Members</h5>
|
||||
<div class="container">
|
||||
{{range .Members}}
|
||||
<div class="row mt-4">
|
||||
|
@ -145,7 +131,7 @@
|
|||
<a href="/admin/ldap/{{.DN}}">{{.DN}}</a>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<form method="POST" onsubmit="return confirm('Supprimer du groupe ?');">
|
||||
<form method="POST" onsubmit="return confirm('Remove from Group ?');">
|
||||
<input type="hidden" name="action" value="delete-member" />
|
||||
<input type="hidden" name="member" value="{{.DN}}" />
|
||||
<input type="submit" value="Supprimer" class="form-control btn btn-danger btn-sm" />
|
||||
|
@ -157,10 +143,10 @@
|
|||
<input type="hidden" name="action" value="add" />
|
||||
<input type="hidden" name="attr" value="member" />
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3"><strong>Ajouter au groupe :</strong>
|
||||
<div class="col-md-3"><strong>Add to Group :</strong>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input class="form-control" type="text" list="users" name="values" placeholder="Utilisateur..." />
|
||||
<input class="form-control" type="text" list="users" name="values" placeholder="User..." />
|
||||
<datalist id="users">
|
||||
{{range .PossibleNewMembers}}
|
||||
<option value="{{.DN}}">{{.Name}}</option>
|
||||
|
@ -168,7 +154,7 @@
|
|||
</datalist>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="submit" value="Ajouter" class="form-control btn btn-success btn-sm" />
|
||||
<input type="submit" value="Add" class="form-control btn btn-success btn-sm" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -176,7 +162,7 @@
|
|||
|
||||
{{if .HasGroups}}
|
||||
<hr class="mt-4" />
|
||||
<h5 class="mt-4">Membre de</h5>
|
||||
<h5 class="mt-4">Member of</h5>
|
||||
<div class="container">
|
||||
{{range .Groups}}
|
||||
<div class="row mt-4">
|
||||
|
@ -187,7 +173,7 @@
|
|||
<a href="/admin/ldap/{{.DN}}">{{.DN}}</a>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<form method="POST" onsubmit="return confirm('Supprimer du groupe ?');">
|
||||
<form method="POST" onsubmit="return confirm('Delete from Group ?');">
|
||||
<input type="hidden" name="action" value="delete-from-group" />
|
||||
<input type="hidden" name="group" value="{{.DN}}" />
|
||||
<input type="submit" value="Supprimer" class="form-control btn btn-danger btn-sm" />
|
||||
|
@ -198,10 +184,10 @@
|
|||
<form method="POST">
|
||||
<input type="hidden" name="action" value="add-to-group" />
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3"><strong>Nouveau groupe :</strong>
|
||||
<div class="col-md-3"><strong>New group :</strong>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input class="form-control" list="groups" type="text" name="group" placeholder="Groupe..." />
|
||||
<input class="form-control" list="groups" type="text" name="group" placeholder="Group..." />
|
||||
<datalist id="groups">
|
||||
{{range .PossibleNewGroups}}
|
||||
<option value="{{.DN}}">{{.Name}}</option>
|
||||
|
@ -209,7 +195,7 @@
|
|||
</datalist>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="submit" value="Ajouter" class="form-control btn btn-success btn-sm" />
|
||||
<input type="submit" value="Add" class="form-control btn btn-success btn-sm" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -217,15 +203,15 @@
|
|||
|
||||
{{if .CanDelete}}
|
||||
<hr class="mt-4">
|
||||
<h5 class="mt-4">Supprimer l'objet</h5>
|
||||
<h5 class="mt-4">Delete the Object</h5>
|
||||
<div class="alert alert-danger">
|
||||
Attention, cette opération est irrévocable !
|
||||
Attention, this action is irreversible !
|
||||
</div>
|
||||
<form method="POST" onsubmit="return confirm('Supprimer cet objet DÉFINITIVEMENT ?');">
|
||||
<form method="POST" onsubmit="return confirm('Do you REALLY want to delete this object forever ?');">
|
||||
<div class="form-row">
|
||||
<input type="hidden" name="action" value="delete-object" />
|
||||
<div class="col-sm-5"></div>
|
||||
<input type="submit" value="Supprimer l'objet" class="form-control btn btn-danger col-sm-2" />
|
||||
<input type="submit" value="DELETE" class="form-control btn btn-danger col-sm-2" />
|
||||
<div class="col-sm-5"></div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
{{define "title"}}Mailing lists |{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
|
||||
<div class="d-flex">
|
||||
<h4>Mailing lists</h4>
|
||||
<a class="ml-auto btn btn-success" href="/admin/create/ml/{{.MailingBaseDN}}">Nouvelle mailing list</a>
|
||||
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
|
||||
</div>
|
||||
|
||||
<table class="table mt-4">
|
||||
<thead>
|
||||
<th scope="col">Adresse</th>
|
||||
<th scope="col">Description</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{with $root := .}}
|
||||
{{range $ml := $root.MailingLists}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/admin/mailing/{{$ml.GetAttributeValue $root.MailingNameAttr}}">
|
||||
{{$ml.GetAttributeValue $root.MailingNameAttr}}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{$ml.GetAttributeValue "description"}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{end}}
|
|
@ -1,117 +0,0 @@
|
|||
{{define "title"}}ML {{.MailingList.GetAttributeValue .MailingNameAttr}} |{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
|
||||
<div class="d-flex">
|
||||
<h4>ML {{.MailingList.GetAttributeValue .MailingNameAttr}}
|
||||
<a class="ml-auto btn btn-sm btn-dark" href="/admin/ldap/{{.MailingList.DN}}">Vue avancée</a>
|
||||
</h4>
|
||||
<a class="ml-auto btn btn-dark" href="/admin/mailing">Liste des ML</a>
|
||||
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
|
||||
</div>
|
||||
|
||||
{{if .Success}}
|
||||
<div class="alert alert-success mt-2">Modification enregistrée.</div>
|
||||
{{end}}
|
||||
{{if .Error}}
|
||||
<div class="alert alert-danger mt-2">
|
||||
Impossible d'effectuer la modification.
|
||||
<div style="font-size: 0.8em">{{.Error}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{with $desc := .MailingList.GetAttributeValue "description"}}{{if $desc}}
|
||||
<p class="mt-4">{{$desc}}</p>
|
||||
{{end}}{{end}}
|
||||
|
||||
<table class="table mt-4">
|
||||
<thead>
|
||||
<th scope="col">Adresse</th>
|
||||
<th scope="col">Nom</th>
|
||||
<th scope="col" style="width: 6em"></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{with $root := .}}
|
||||
{{range $member := $root.Members}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/admin/ldap/{{$member.DN}}">
|
||||
{{$member.GetAttributeValue "mail"}}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{$member.GetAttributeValue "displayname"}}</td>
|
||||
<td>
|
||||
<form method="POST" onsubmit="return confirm('Supprimer de la ML ?');">
|
||||
<input type="hidden" name="action" value="delete-member" />
|
||||
<input type="hidden" name="member" value="{{.DN}}" />
|
||||
<input type="submit" value="Suppr" class="form-control btn btn-danger btn-sm" />
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if not .Members}}
|
||||
<tr><td>(aucun abonné)</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<hr class="mt-4" />
|
||||
<h5 class="mt-4">Ajouter un destinataire</h5>
|
||||
|
||||
<div class="container">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="add-member" />
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3"><strong>Utilisateur existant :</strong> </div>
|
||||
<div class="col-md-5">
|
||||
<input class="form-control" type="text" list="users" name="member" placeholder="Utilisateur..." />
|
||||
<datalist id="users">
|
||||
{{range .PossibleNewMembers}}
|
||||
{{if .GetAttributeValue "mail"}}
|
||||
<option value="{{.DN}}">{{if .GetAttributeValue "displayname"}}{{.GetAttributeValue "displayname"}} ({{.GetAttributeValue "mail" }}){{else}}{{.GetAttributeValue "mail"}}{{end}}</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="submit" value="Ajouter" class="form-control btn btn-success btn-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{if .AllowGuest}}
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-10">OU</div>
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="add-external" />
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3"><strong>E-mail :</strong></div>
|
||||
<div class="col-md-5">
|
||||
<input class="form-control" type="text" name="mail" placeholder="machin@truc.net..." />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3"><strong>Nom (optionnel) :</strong></div>
|
||||
<div class="col-md-5">
|
||||
<input class="form-control" type="text" name="displayname" placeholder="Machin Truc..." />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="submit" value="Ajouter" class="form-control btn btn-success btn-sm" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<small class="form-text text-muted col-md-10">
|
||||
Si un utilisateur existe déjà avec l'email spécifiée, celui-ci sera ajouté à la liste.
|
||||
Sinon, un utilisateur invité sera créé.
|
||||
</small>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{end}}
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
{{define "body"}}
|
||||
<div class="d-flex">
|
||||
<h4>Annuaire</h4>
|
||||
<a class="ml-auto btn btn-info" href="/">Menu principal</a>
|
||||
<h4>Directory</h4>
|
||||
<a class="ml-auto btn btn-info" href="/">Main Menu</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
|||
<form>
|
||||
<div class="form-group form-row">
|
||||
<div class="col-sm-2"> </div>
|
||||
<label for="search" class="col-sm-2 col-form-label">Rechercher :</label>
|
||||
<label for="search" class="col-sm-2 col-form-label">Search for :</label>
|
||||
<input class="form-control col-sm-4" id="search" name="search" type="text" onkeyup="searchDirectory()" size="20">
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -19,5 +19,5 @@
|
|||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
Aucun résultat.
|
||||
No results found.
|
||||
{{end}}
|
||||
|
|
|
@ -1,234 +0,0 @@
|
|||
{{define "title"}}Profile |{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="d-flex">
|
||||
<h4>Mes identifiants</h4>
|
||||
<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>
|
||||
|
||||
<ul class="nav nav-tabs" id="proto" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="s3-tab" data-toggle="tab" href="#s3" role="tab" aria-controls="s3" aria-selected="true">S3</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="sftp-tab" data-toggle="tab" href="#sftp" role="tab" aria-controls="sftp" aria-selected="false">SFTP</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="protocols">
|
||||
<div class="tab-pane fade show active" id="s3" role="tabpanel" aria-labelledby="s3-tab">
|
||||
<table class="table mt-4">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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><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>
|
||||
<td>garage</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Endpoint URL</th>
|
||||
<td>https://garage.deuxfleurs.fr</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Type d'URL</th>
|
||||
<td>DNS et chemin (préférer chemin)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Signature</th>
|
||||
<td>Version 4</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Configurer votre logiciel :</p>
|
||||
|
||||
<div class="accordion" id="softconfig">
|
||||
<div class="card">
|
||||
<div class="card-header" id="awscli-title">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#awscli" aria-expanded="false" aria-controls="awscli">
|
||||
awscli
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<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={{ .S3KeyInfo.AccessKeyId }}
|
||||
export AWS_SECRET_ACCESS_KEY={{ .S3KeyInfo.SecretAccessKey }}
|
||||
export AWS_DEFAULT_REGION='garage'
|
||||
|
||||
function aws { command aws --endpoint-url https://garage.deuxfleurs.fr $@ ; }
|
||||
aws --version
|
||||
</pre>
|
||||
<p>Ensuite vous pouvez utiliser awscli :</p>
|
||||
<pre>
|
||||
source ~/.awsrc
|
||||
aws s3 ls
|
||||
aws s3 ls s3://my-bucket
|
||||
aws s3 cp /tmp/a.txt s3://my-bucket
|
||||
...
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header" id="minio-title">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#minio" aria-expanded="true" aria-controls="minio">
|
||||
Minio CLI
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="minio" class="collapse" aria-labelledby="minio-title" data-parent="#softconfig">
|
||||
<div class="card-body">
|
||||
<p>Vous pouvez configurer Minio CLI avec cette commande :</p>
|
||||
<pre>
|
||||
mc alias set \
|
||||
garage \
|
||||
https://garage.deuxfleurs.fr \
|
||||
{{ .S3KeyInfo.AccessKeyId }} \
|
||||
{{ .S3KeyInfo.SecretAccessKey }} \
|
||||
--api S3v4
|
||||
</pre>
|
||||
<p>Et ensuite pour utiliser Minio CLI avec :</p>
|
||||
<pre>
|
||||
mc ls garage/
|
||||
mc cp /tmp/a.txt garage/my-bucket/a.txt
|
||||
...
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header" id="winscp-title">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#winscp" aria-expanded="true" aria-controls="winscp">
|
||||
WinSCP
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="winscp" class="collapse" aria-labelledby="winscp-title" data-parent="#softconfig">
|
||||
<div class="card-body">
|
||||
Reportez vous <a href="">au guide</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header" id="hugo-title">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#hugo" aria-expanded="false" aria-controls="hugo">
|
||||
Hugo
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="hugo" class="collapse" aria-labelledby="hugo-title" data-parent="#softconfig">
|
||||
<div class="card-body">
|
||||
<p>Dans votre fichier <code>config.toml</code>, rajoutez :</p>
|
||||
<pre>
|
||||
[[deployment.targets]]
|
||||
URL = "s3://bucket?endpoint=garage.deuxfleurs.fr&s3ForcePathStyle=true&region=garage"
|
||||
</pre>
|
||||
<p>Assurez-vous d'avoir un fichier dans lequel les variables <code>AWS_ACCESS_KEY_ID</code> et <code>AWS_SECRET_ACCESS_KEY</code> sont définies,
|
||||
ici on suppose que vous avez suivi les instructions de l'outil awscli (ci-dessus) et que vous avez un fichier <code>~/.awsrc</code> qui défini ces variables.
|
||||
Ensuite : </p>
|
||||
<pre>
|
||||
source ~/.awsrc
|
||||
hugo deploy
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header" id="publii-title">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#publii" aria-expanded="false" aria-controls="publii">
|
||||
Publii
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="publii" class="collapse" aria-labelledby="publii-title" data-parent="#softconfig">
|
||||
<div class="card-body">
|
||||
<em>Bientôt...</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- sftp -->
|
||||
<div class="tab-pane fade" id="sftp" role="tabpanel" aria-labelledby="sftp-tab">
|
||||
<table class="table mt-4">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Nom d'utilisateur-ice</th>
|
||||
<td>{{ .Login.Info.Username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Mot de passe</th>
|
||||
<td>(votre mot de passe guichet)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Hôte</th>
|
||||
<td>sftp://bagage.deuxfleurs.fr</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Port</th>
|
||||
<td>2222</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Configurer votre logiciel :</p>
|
||||
|
||||
<div class="accordion" id="softconfig2">
|
||||
<div class="card">
|
||||
<div class="card-header" id="filezilla-title">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#filezilla" aria-expanded="false" aria-controls="filezilla">
|
||||
scp
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="filezilla" class="collapse show" aria-labelledby="filezilla-title" data-parent="#softconfig">
|
||||
<div class="card-body">
|
||||
<p>Un exemple avec SCP :</p>
|
||||
<pre>
|
||||
scp -oHostKeyAlgorithms=+ssh-rsa -P2222 -r ./public {{ .Login.Info.Username }}@bagage.deuxfleurs.fr:mon_bucket/
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header" id="filezilla-title">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#filezilla" aria-expanded="false" aria-controls="filezilla">
|
||||
Filezilla
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="filezilla" class="collapse" aria-labelledby="filezilla-title" data-parent="#softconfig">
|
||||
<div class="card-body">
|
||||
<em>Bientôt</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{end}}
|
|
@ -1,72 +0,0 @@
|
|||
{{define "title"}}Créer un site web |{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="d-flex">
|
||||
<h4>Modifier le nom de domaine</h4>
|
||||
<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">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="dnsint-tab" data-toggle="tab" href="#dnsint" role="tab" aria-controls="dnsint" aria-selected="true">Je n'ai pas de nom de domaine</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="dnsext-tab" data-toggle="tab" href="#dnsext" role="tab" aria-controls="dnsext" aria-selected="false">Utiliser mon propre nom de domaine</a>
|
||||
</li>
|
||||
</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">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="bucket">Sous-domaine désiré :</label>
|
||||
<input type="text" id="bucket" name="bucket" placeholder="mon-site" class="form-control" value="" onkeyup="document.getElementById('url').value = `https://${document.getElementById('bucket').value}.web.deuxfleurs.fr`" />
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="url">Votre site sera accessible à l'URL suivante :</label>
|
||||
<input type="text" id="url" disabled="true" name="url" class="form-control" value="https://mon-site.web.deuxfleurs.fr" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p>La première fois que vous chargerez votre site web, une erreur de certificat sera renvoyée. C'est normal, il faudra patienter quelques minutes le temps que le certificat se génère.</p>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Modifier le nom de domaine</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane fade show" id="dnsext" role="tabpanel" aria-labelledby="dnsext-tab">
|
||||
<form method="POST" class="mt-4">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="bucket2">Votre nom de domaine :</label>
|
||||
<input type="text" id="bucket2" name="bucket2" placeholder="example.com" class="form-control" value="" onkeyup="document.getElementById('url2').value = `https://${document.getElementById('bucket2').value}`" />
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="url2">Votre site sera accessible à l'URL suivante :</label>
|
||||
<input type="text" id="url2" disabled="true" name="url2" class="form-control" value="https://example.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>Vous devez éditer votre zone DNS, souvent gérée par votre bureau d'enregistrement, comme Gandi, pour la faire pointer vers Deuxfleurs. Si vous utilisez un sous domaine (eg. <code>site.example.com</code>), une entrée <code>CNAME</code> est appropriée :</p>
|
||||
<pre>site CNAME 3600 garage.deuxfleurs.fr.</pre>
|
||||
<p>Si vous utilisez la racine de votre nom de domaine (eg. <code>example.com</code>, aussi appelée APEX), la solution dépend de votre fournisseur DNS, il vous faudra au choix une entrée <code>ALIAS</code> ou <code>CNAME</code> en fonction de ce que votre fournisseur supporte :</p>
|
||||
<pre>@ ALIAS 3600 garage.deuxfleurs.fr.</pre>
|
||||
<p>La première fois que vous chargerez votre site web, une erreur de certificat sera renvoyée. C'est normal, il faudra patienter quelques minutes le temps que le certificat se génère.</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">Modifier le nom de domaine</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
|
@ -1,81 +0,0 @@
|
|||
{{define "title"}}Inspecter le site web |{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="d-flex">
|
||||
<!--<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>
|
||||
|
||||
<div class="row">
|
||||
{{ if .Err }}
|
||||
<div class="col-md-12 mt-3">
|
||||
<div class="alert alert-danger">{{ .Err.Error }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<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>
|
||||
|
||||
<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.Percent }}%
|
||||
</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" href="/website/vhost/{{ .View.Name.Pretty }}">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 }}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
{{define "title"}}Créer un site web |{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="d-flex">
|
||||
<h4>Créer un site web</h4>
|
||||
<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">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="dnsint-tab" data-toggle="tab" href="#dnsint" role="tab" aria-controls="dnsint" aria-selected="true">Je n'ai pas de nom de domaine</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="dnsext-tab" data-toggle="tab" href="#dnsext" role="tab" aria-controls="dnsext" aria-selected="false">Utiliser mon propre nom de domaine</a>
|
||||
</li>
|
||||
</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">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="bucket">Sous-domaine désiré :</label>
|
||||
<input type="text" id="bucket" name="bucket" placeholder="mon-site" class="form-control" value="" onkeyup="document.getElementById('url').value = `https://${document.getElementById('bucket').value}.web.deuxfleurs.fr`" />
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="url">Votre site sera accessible à l'URL suivante :</label>
|
||||
<input type="text" id="url" disabled="true" name="url" class="form-control" value="https://mon-site.web.deuxfleurs.fr" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p>La première fois que vous chargerez votre site web, une erreur de certificat sera renvoyée. C'est normal, il faudra patienter quelques minutes le temps que le certificat se génère.</p>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Créer un nouveau site web</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane fade show" id="dnsext" role="tabpanel" aria-labelledby="dnsext-tab">
|
||||
<form method="POST" class="mt-4">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="bucket2">Votre nom de domaine :</label>
|
||||
<input type="text" id="bucket2" name="bucket2" placeholder="example.com" class="form-control" value="" onkeyup="document.getElementById('url2').value = `https://${document.getElementById('bucket2').value}`" />
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="url2">Votre site sera accessible à l'URL suivante :</label>
|
||||
<input type="text" id="url2" disabled="true" name="url2" class="form-control" value="https://example.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>Vous devez éditer votre zone DNS, souvent gérée par votre bureau d'enregistrement, comme Gandi, pour la faire pointer vers Deuxfleurs. Si vous utilisez un sous domaine (eg. <code>site.example.com</code>), une entrée <code>CNAME</code> est appropriée :</p>
|
||||
<pre>site CNAME 3600 garage.deuxfleurs.fr.</pre>
|
||||
<p>Si vous utilisez la racine de votre nom de domaine (eg. <code>example.com</code>, aussi appelée APEX), la solution dépend de votre fournisseur DNS, il vous faudra au choix une entrée <code>ALIAS</code> ou <code>CNAME</code> en fonction de ce que votre fournisseur supporte :</p>
|
||||
<pre>@ ALIAS 3600 garage.deuxfleurs.fr.</pre>
|
||||
<p>La première fois que vous chargerez votre site web, une erreur de certificat sera renvoyée. C'est normal, il faudra patienter quelques minutes le temps que le certificat se génère.</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">Créer un nouveau site web</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
|
@ -2,59 +2,45 @@
|
|||
|
||||
{{define "body"}}
|
||||
<div class="alert alert-info">
|
||||
Bienvenue, <strong>{{ .User.WelcomeName }}</strong> !
|
||||
Welcome, <strong>{{ .Login.WelcomeName }}</strong> !
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a class="ml-auto btn btn-sm btn-dark" href="/logout">Se déconnecter</a>
|
||||
<a class="ml-auto btn btn-sm btn-dark" href="/logout">Disconnect</a>
|
||||
</div>
|
||||
<div class="mt-3"></div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Mon compte
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action" href="/profile">Modifier mon profil</a>
|
||||
<a class="list-group-item list-group-item-action" href="/passwd">Modifier mon mot de passe</a>
|
||||
<a class="list-group-item list-group-item-action" href="/directory">Annuaire</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Mon espace sur la toile
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<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 .User.Capabilities.CanInvite}}
|
||||
<div class="card mt-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Inviter des gens sur Deuxfleurs
|
||||
My Account
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action" href="/invite/send_code">Envoyer un code d'invitation</a>
|
||||
<a class="list-group-item list-group-item-action" href="/invite/new_account">Créer un nouveau compte directement</a>
|
||||
<a class="list-group-item list-group-item-action" href="/profile">Modify My Profile</a>
|
||||
<a class="list-group-item list-group-item-action" href="/passwd">Modify My Password</a>
|
||||
<a class="list-group-item list-group-item-action" href="/directory">Directory</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .Login.CanInvite}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
Invite People To Deuxfleurs
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action" href="/invite/send_code">Send an invite code</a>
|
||||
<a class="list-group-item list-group-item-action" href="/invite/new_account">Create a new Account</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .User.Capabilities.CanAdmin}}
|
||||
{{if .Login.CanAdmin}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
Administration
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action" href="/admin/users">Utilisateur·ices</a>
|
||||
<a class="list-group-item list-group-item-action" href="/admin/groups">Groupes</a>
|
||||
<a class="list-group-item list-group-item-action" href="/admin/mailing">Mailing lists</a>
|
||||
<a class="list-group-item list-group-item-action" href="/admin/ldap/{{.BaseDN}}">Explorateur LDAP</a>
|
||||
<a class="list-group-item list-group-item-action" href="/admin/users">Users</a>
|
||||
<a class="list-group-item list-group-item-action" href="/admin/groups">Groups</a>
|
||||
<a class="list-group-item list-group-item-action" href="/admin/ldap/{{.BaseDN}}">Explore the Directory</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</div>
|
||||
{{end}}
|
||||
{{if .WarningMessage}}
|
||||
<div class="alert alert-danger mt-4">Des erreurs se sont produites, le compte pourrait ne pas être totalement fonctionnel.
|
||||
<div class="alert alert-danger mt-4">Des erreurs se sont produtes, le compte pourrait ne pas être totalement fonctionnel.
|
||||
<div style="font-size: 0.8em">{{ .WarningMessage }}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
@ -25,15 +25,12 @@
|
|||
<form method="POST" class="mt-4">
|
||||
<h5>Renseignements obligatoires</h5>
|
||||
<div class="form-group">
|
||||
<label for="username">Identifiant souhaité :</label>
|
||||
<label for="username">Nom d'utilisateur souhaité :</label>
|
||||
<input type="text" id="username" name="username" class="form-control" value="{{ .Username }}" />
|
||||
<small class="form-text text-muted">
|
||||
Votre identifiant doit être en minuscule.
|
||||
</small>
|
||||
</div>
|
||||
{{if .ErrorInvalidUsername}}
|
||||
<div class="alert alert-warning">
|
||||
Nom d'utilisateur invalide. Ne peut contenir que les caractères suivants : chiffres, lettres minuscules, point, tiret bas (_) et tiret du milieu (-).
|
||||
Nom d'utilisateur invalide. Ne peut contenir que les caractères suivants : chiffres, lettres, point, tiret bas (_) et tiret du milieu (-).
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .ErrorUsernameTaken}}
|
||||
|
@ -44,9 +41,6 @@
|
|||
<div class="form-group">
|
||||
<label for="password">Mot de passe :</label>
|
||||
<input type="password" id="password" name="password" class="form-control" />
|
||||
<small class="form-text text-muted">
|
||||
La seule contrainte est que votre mot de passe doit faire au moins 8 caractères. Utilisez chiffres, majuscules, et caractères spéciaux sans modération !
|
||||
</small>
|
||||
</div>
|
||||
{{if .ErrorPasswordTooShort}}
|
||||
<div class="alert alert-warning">
|
||||
|
@ -64,9 +58,6 @@
|
|||
{{end}}
|
||||
|
||||
<h5>Renseignements optionnels</h5>
|
||||
<small class="form-text text-muted">
|
||||
Ces informations sont utilisées exclusivement pour "pré-configurer" les services que vous utiliserez, elles n'ont pas besoin de correspondre à votre état civil.
|
||||
</small>
|
||||
<div class="form-group">
|
||||
<label for="displayname">Nom complet :</label>
|
||||
<input type="text" id="displayname" name="displayname" class="form-control" value="{{ .DisplayName }}" />
|
||||
|
|
|
@ -6,15 +6,13 @@
|
|||
|
||||
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
|
||||
|
||||
<title>{{template "title" .}} Guichet</title>
|
||||
<title>{{template "title"}} Guichet</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mb-4">
|
||||
<div class="container">
|
||||
<h1>Guichet Deuxfleurs💮💮</h1>
|
||||
<hr />
|
||||
{{template "body" .}}
|
||||
</div>
|
||||
<script src="/static/javascript/jquery.slim.min.js"></script>
|
||||
<script src="/static/javascript/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<form method="POST">
|
||||
{{if .WrongUser}}
|
||||
<div class="alert alert-danger">Identifiant invalide.</div>
|
||||
<div class="alert alert-danger">Nom d'utilisateur invalide.</div>
|
||||
{{end}}
|
||||
{{if .WrongPass}}
|
||||
<div class="alert alert-danger">Mot de passe invalide.</div>
|
||||
|
@ -16,11 +16,11 @@
|
|||
</div>
|
||||
{{end}}
|
||||
<div class="form-group">
|
||||
<label for="username">Identifiant :</label>
|
||||
<label for="username">Nom d'utilisateur:</label>
|
||||
<input type="text" name="username" id="username" class="form-control" value="{{ .Username }}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Mot de passe :</label>
|
||||
<label for="password">Mot de passe:</label>
|
||||
<input type="password" name="password" id="password" class="form-control" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Se connecter</button>
|
||||
|
@ -30,6 +30,6 @@
|
|||
|
||||
<p><strong>Mot de passe oublié ?</strong>
|
||||
Écrivez à <samp>coucou</samp><img src="static/image/at_sign.svg" style="height: 1em" alt="arobase" /><samp>deuxfleurs.fr</samp>
|
||||
ou contactez directement votre opérateur·ice préféré·e.</p>
|
||||
ou contactez directement votre administrateur favori.</p>
|
||||
|
||||
{{end}}
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
<form method="POST" class="mt-4" enctype="multipart/form-data">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label>Identifiant:</label>
|
||||
<input type="text" disabled="true" class="form-control" value="{{ .User.Login.Info.Username }}" />
|
||||
<label>Nom d'utilisateur:</label>
|
||||
<input type="text" disabled="true" class="form-control" value="{{ .Status.Info.Username }}" />
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="mail">Adresse e-mail:</label>
|
||||
|
|
251
website.go
251
website.go
|
@ -1,251 +0,0 @@
|
|||
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")
|
||||
ErrCantChangeVhost = fmt.Errorf("Can't change the vhost to the desired value. Maybe it's already used by someone else or an internal error occured")
|
||||
ErrCantRemoveOldVhost = fmt.Errorf("The new vhost is bound to the bucket but the old one can't be removed, this is an internal error")
|
||||
)
|
||||
|
||||
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 "update bucket" function
|
||||
binfo, err = grgUpdateBucket(website.Internal, ur)
|
||||
if err != nil {
|
||||
return nil, ErrCantConfigureBucket
|
||||
}
|
||||
|
||||
// Update the alias if the vhost field is set and different
|
||||
if patch.Vhost != nil && *patch.Vhost != "" && *patch.Vhost != pretty {
|
||||
binfo, err = grgAddGlobalAlias(website.Internal, *patch.Vhost)
|
||||
if err != nil {
|
||||
return nil, ErrCantChangeVhost
|
||||
}
|
||||
binfo, err = grgDelGlobalAlias(website.Internal, pretty)
|
||||
if err != nil {
|
||||
return nil, ErrCantRemoveOldVhost
|
||||
}
|
||||
}
|
||||
|
||||
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:"vhost"`
|
||||
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"`
|
||||
Vhost *string `json:"vhost"`
|
||||
}
|
Loading…
Reference in New Issue