Compare commits

..

4 Commits
main ... main

Author SHA1 Message Date
thiesjohannbroetje 150aa37cc1 translated admin_ldap page to English
Signed-off-by: thiesjohannbroetje <kontakt+dev@thiesbroetje.de>
2021-09-11 16:37:10 +02:00
thiesjohannbroetje 8c02d4f827 translated directory page to English 2021-09-11 16:28:28 +02:00
thiesjohannbroetje 4f207e6ed5 translated results page to English
Signed-off-by: thiesjohannbroetje <kontakt+dev@thiesbroetje.de>
2021-09-11 16:26:56 +02:00
thiesjohannbroetje 4c7250df42 translated home to English 2021-09-11 16:25:40 +02:00
48 changed files with 454 additions and 3206 deletions

13
.drone.yml Normal file
View File

@ -0,0 +1,13 @@
---
pipeline:
build:
image: golang:stretch
commands:
- go get -d -v
- go build -v
---
kind: signature
hmac: 38948cd073f3a0b73ab7bb13ba1b5e18c64c02976abfd6dcd5bf7a4c34197e8c
...

1
.envrc
View File

@ -1 +0,0 @@
use flake

3
.gitignore vendored
View File

@ -1,6 +1,3 @@
guichet
guichet.static
config.json
result
.direnv/
password

View File

@ -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 .

View File

@ -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
View File

@ -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
View File

@ -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
View File

@ -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)
}

View File

@ -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",

View File

@ -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

View File

@ -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
}

View File

@ -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
View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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="

View File

@ -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)

View File

@ -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:*"
]
}

View File

@ -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="

View File

@ -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"
}

View File

@ -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"

View File

@ -21,29 +21,29 @@ import (
var EMAIL_REGEXP = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
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
View File

@ -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
View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
View File

@ -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
View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View File

@ -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}}

View File

@ -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">&nbsp;</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>

View File

@ -19,5 +19,5 @@
</div>
{{end}}
{{else}}
Aucun résultat.
No results found.
{{end}}

View File

@ -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&amp;s3ForcePathStyle=true&amp;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}}

View File

@ -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}}

View File

@ -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 }}

View File

@ -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}}

View File

@ -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}}

View File

@ -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 }}" />

View File

@ -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>

View File

@ -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}}

View File

@ -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>

View File

@ -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"`
}