From 02e384f99eb10ab299805073c37ac2f4ed3f2cdc Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 15 Sep 2023 13:59:48 +0200 Subject: [PATCH 01/14] extract "build DN logic" --- main.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index ae8fe06..92fd2dc 100644 --- a/main.go +++ b/main.go @@ -198,6 +198,9 @@ func logRequest(handler http.Handler) http.Handler { func checkLogin(w http.ResponseWriter, r *http.Request) *LoginStatus { var login_info *LoginInfo + //@FIXME check authentication header + + session, err := store.Get(r, SESSION_NAME) if err == nil { username, ok := session.Values["login_username"] @@ -364,6 +367,15 @@ type LoginFormData struct { ErrorMessage string } +func buildUserDN(username string) string { + user_dn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, username, config.UserBaseDN) + if strings.EqualFold(username, config.AdminAccount) { + user_dn = username + } + + return user_dn +} + func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo { templateLogin := getTemplate("login.html") @@ -375,10 +387,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo { username := strings.Join(r.Form["username"], "") password := strings.Join(r.Form["password"], "") - user_dn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, username, config.UserBaseDN) - if strings.EqualFold(username, config.AdminAccount) { - user_dn = username - } + user_dn := buildUserDN(username) l := ldapOpen(w) if l == nil { From 74113fad490ccdaa00961c5818eaa107781dfd79 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 15 Sep 2023 14:32:44 +0200 Subject: [PATCH 02/14] WIP auth API --- api.go | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 5 +-- 2 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 api.go diff --git a/api.go b/api.go new file mode 100644 index 0000000..1007914 --- /dev/null +++ b/api.go @@ -0,0 +1,114 @@ +package main + +import ( + //"context" + //"errors" + "fmt" + //garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" + "github.com/go-ldap/ldap/v3" + //"github.com/gorilla/mux" + "log" + "net/http" + "strings" +) + +func checkLoginAPI(w http.ResponseWriter, r *http.Request) *LoginStatus { + username, password, ok := r.BasicAuth() + if !ok { + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return nil + } + user_dn := buildUserDN(username) + + login_info := &LoginInfo{ + DN: user_dn, + Username: username, + Password: password, + } + + l := ldapOpen(w) + if l == nil { + log.Println(l) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return nil + } + + err := l.Bind(login_info.DN, login_info.Password) + if err != nil { + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return nil + } + + loginStatus := &LoginStatus{ + Info: login_info, + conn: l, + } + + requestKind := "(objectClass=organizationalPerson)" + + if strings.EqualFold(login_info.DN, config.AdminAccount) { + requestKind = "(objectclass=*)" + } + searchRequest := ldap.NewSearchRequest( + login_info.DN, + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, + requestKind, + []string{ + "dn", + "displayname", + "givenname", + "sn", + "mail", + "memberof", + "description", + "garage_s3_access_key", + FIELD_NAME_DIRECTORY_VISIBILITY, + FIELD_NAME_PROFILE_PICTURE, + }, + nil) + + sr, err := l.Search(searchRequest) + if err != nil { + log.Println(err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return nil + } + + if len(sr.Entries) != 1 { + log.Println(fmt.Sprintf("Unable to find entry for %s", login_info.DN)) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return nil + } + + 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 handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) { + login, s3key, err := checkLoginAndS3(w, r) + if err != nil { + log.Println(err) + return + } + log.Println(login,s3key) + + return +} diff --git a/main.go b/main.go index 92fd2dc..1402ff2 100644 --- a/main.go +++ b/main.go @@ -130,6 +130,8 @@ func main() { r.HandleFunc("/", handleHome) r.HandleFunc("/logout", handleLogout) + r.HandleFunc("/api/unstable/garage/bucket/{b}", handleAPIGarageBucket) + r.HandleFunc("/profile", handleProfile) r.HandleFunc("/passwd", handlePasswd) r.HandleFunc("/picture/{name}", handleDownloadPicture) @@ -198,9 +200,6 @@ func logRequest(handler http.Handler) http.Handler { func checkLogin(w http.ResponseWriter, r *http.Request) *LoginStatus { var login_info *LoginInfo - //@FIXME check authentication header - - session, err := store.Get(r, SESSION_NAME) if err == nil { username, ok := session.Values["login_username"] From 9c21c2e799449ae303ed764ba1677366d0571e8d Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 15 Sep 2023 14:38:46 +0200 Subject: [PATCH 03/14] split LDAP and S3 --- garage.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/garage.go b/garage.go index 1ae02e4..61d1b93 100644 --- a/garage.go +++ b/garage.go @@ -109,17 +109,15 @@ func grgGetBucket(bid string) (*garage.BucketInfo, error) { } -func checkLoginAndS3(w http.ResponseWriter, r *http.Request) (*LoginStatus, *garage.KeyInfo, error) { - login := checkLogin(w, r) +func checkS3(login *LoginStatus) (*garage.KeyInfo, error) { if login == nil { - return nil, nil, errors.New("LDAP login failed") + return nil, errors.New("Login can't be nil") } - keyID := login.UserEntry.GetAttributeValue("garage_s3_access_key") if keyID == "" { keyPair, err := grgCreateKey(login.Info.Username) if err != nil { - return login, nil, err + return nil, err } modify_request := ldap.NewModifyRequest(login.Info.DN, nil) modify_request.Replace("garage_s3_access_key", []string{*keyPair.AccessKeyId}) @@ -128,11 +126,20 @@ func checkLoginAndS3(w http.ResponseWriter, r *http.Request) (*LoginStatus, *gar // or when bottin will be able to dynamically fetch it. modify_request.Replace("garage_s3_secret_key", []string{*keyPair.SecretAccessKey}) err = login.conn.Modify(modify_request) - return login, keyPair, err + return keyPair, err } // Note: we could simply return the login info, but LX asked we do not // store the secrets in LDAP in the future. keyPair, err := grgGetKey(keyID) + return keyPair, err +} + +func checkLoginAndS3(w http.ResponseWriter, r *http.Request) (*LoginStatus, *garage.KeyInfo, error) { + login := checkLogin(w, r) + if login == nil { + return nil, nil, errors.New("LDAP login failed") + } + keyPair, err := checkS3(login) return login, keyPair, err } From f8f417906ad9026cdf2ae6ac18a918cd4069312a Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 15 Sep 2023 14:43:55 +0200 Subject: [PATCH 04/14] Implemented new authentication --- api.go | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/api.go b/api.go index 1007914..9f3ef7b 100644 --- a/api.go +++ b/api.go @@ -2,9 +2,9 @@ package main import ( //"context" - //"errors" + "errors" "fmt" - //garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" + garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" "github.com/go-ldap/ldap/v3" //"github.com/gorilla/mux" "log" @@ -12,12 +12,12 @@ import ( "strings" ) -func checkLoginAPI(w http.ResponseWriter, r *http.Request) *LoginStatus { +func checkLoginAPI(w http.ResponseWriter, r *http.Request) (*LoginStatus, error) { username, password, ok := r.BasicAuth() if !ok { w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) http.Error(w, "Unauthorized", http.StatusUnauthorized) - return nil + return nil, errors.New("Missing or invalid 'Authenticate: Basic' field") } user_dn := buildUserDN(username) @@ -31,14 +31,14 @@ func checkLoginAPI(w http.ResponseWriter, r *http.Request) *LoginStatus { if l == nil { log.Println(l) http.Error(w, "Internal server error", http.StatusInternalServerError) - return nil + return nil, errors.New("Unable to open LDAP connection") } err := l.Bind(login_info.DN, login_info.Password) if err != nil { w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) http.Error(w, "Unauthorized", http.StatusUnauthorized) - return nil + return nil, errors.New("Unable to bind this user+password combination on the LDAP server") } loginStatus := &LoginStatus{ @@ -73,13 +73,13 @@ func checkLoginAPI(w http.ResponseWriter, r *http.Request) *LoginStatus { if err != nil { log.Println(err) http.Error(w, "Internal server error", http.StatusInternalServerError) - return nil + return nil, errors.New("Unable to search essential information about the logged user on LDAP") } if len(sr.Entries) != 1 { log.Println(fmt.Sprintf("Unable to find entry for %s", login_info.DN)) http.Error(w, "Internal server error", http.StatusInternalServerError) - return nil + return nil, errors.New("Not enough or too many entries for this user in the LDAP directory (expect a unique result)") } loginStatus.UserEntry = sr.Entries[0] @@ -99,16 +99,26 @@ func checkLoginAPI(w http.ResponseWriter, r *http.Request) *LoginStatus { } } - return loginStatus + return loginStatus, nil +} + +func checkLoginAndS3API(w http.ResponseWriter, r *http.Request) (*LoginStatus, *garage.KeyInfo, error) { + login, err := checkLoginAPI(w, r) + if err != nil { + return nil, nil, err + } + keyPair, err := checkS3(login) + return login, keyPair, err } func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) { - login, s3key, err := checkLoginAndS3(w, r) + login, s3key, err := checkLoginAndS3API(w, r) if err != nil { log.Println(err) return } - log.Println(login,s3key) + + log.Println(login, s3key) return } From e876bdd6632e0f00e2973cfba5336128ef00a3ab Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 15 Sep 2023 14:59:23 +0200 Subject: [PATCH 05/14] spec --- api.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/api.go b/api.go index 9f3ef7b..bce9993 100644 --- a/api.go +++ b/api.go @@ -118,6 +118,35 @@ func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) { return } + // CHECK PATCH REQUEST + + // READ BODY JSON + + // VALIDATE OBJECT + // --- bucket query parameter --- + // 1. bucket must be owned by the key with owner permission, otherwise throw "unauthorized" (401) + // 2. must not end with deuxfleurs.fr or deuxfleurs.org, otherwise throw "forbidden" (403) + // --- global --- + // 1. can be true, false, or nil (use pointers) + // 2. if nil do nothing + // 3. if false, throw "not yet implemented" (501) + // 4. if true, check that the bucket name does not exist yet in the global namespace, throw "forbidden" (403) + // --- quota.size --- + // 1. if no quota on the bucket + this field is none, set to 50MB + // 2. if lower than 50MB, set to 50MB. If higher than 200MB, set to 200MB + // --- quota.files --- + // 1. if no quota on the bucket + this field is none, set to 10k + // 2. if lower than 10k, set to 10k. If higher than 40k, set to 40k + + // IF BODY.GLOBAL is not NONE + // Add an alias + + // IF BODY.QUOTA.SIZE is not NONE + // Change quota + + // IF BODY.QUOTA.FILE is not NONE + // Change quota + log.Println(login, s3key) return From 5b246ec86bc3eee768da2347f031b349d1e1553d Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 15 Sep 2023 18:25:37 +0200 Subject: [PATCH 06/14] basic logic for switching alias from local/global --- api.go | 153 ++++++++++++++++++++++++++++++++++++++++++++++++------ garage.go | 46 +++++++++++++++- main.go | 2 +- 3 files changed, 184 insertions(+), 17 deletions(-) diff --git a/api.go b/api.go index bce9993..1ddb4ea 100644 --- a/api.go +++ b/api.go @@ -2,11 +2,12 @@ package main import ( //"context" + "encoding/json" "errors" "fmt" garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" "github.com/go-ldap/ldap/v3" - //"github.com/gorilla/mux" + "github.com/gorilla/mux" "log" "net/http" "strings" @@ -111,21 +112,132 @@ func checkLoginAndS3API(w http.ResponseWriter, r *http.Request) (*LoginStatus, * return login, keyPair, err } +type ApiQuotaView struct { + files *uint64 + size *uint64 +} + +type ApiBucketView struct { + global *bool + max *ApiQuotaView + used *ApiQuotaView +} + +type BucketRequest struct { + s3key *garage.KeyInfo + bucketName string + bucketId string + global bool + http *http.Request +} + func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) { - login, s3key, err := checkLoginAndS3API(w, r) + br, err := buildBucketRequest(w, r) if err != nil { - log.Println(err) return } - // CHECK PATCH REQUEST + if r.Method == http.MethodPatch { + patchGarageBucket(w, br) + return + } - // READ BODY JSON + if r.Method == http.MethodGet { + getGarageBucket(w, br) + return + } - // VALIDATE OBJECT - // --- bucket query parameter --- - // 1. bucket must be owned by the key with owner permission, otherwise throw "unauthorized" (401) - // 2. must not end with deuxfleurs.fr or deuxfleurs.org, otherwise throw "forbidden" (403) + http.Error(w, "This method is not implemented for this endpoint", http.StatusNotImplemented) + return +} + +func buildBucketRequest(w http.ResponseWriter, r *http.Request) (*BucketRequest, error) { + _, s3key, err := checkLoginAndS3API(w, r) + if err != nil { + http.Error(w, "Unable to connect on LDAP", http.StatusUnauthorized) + return nil, err + } + + // FETCH BUCKET ID by iterating over buckets owned by this key + bucketName := mux.Vars(r)["bucket"] + var bucketId *string + var global *bool + +findBucketIdLoop: + for _, bucket := range s3key.Buckets { + for _, localAlias := range bucket.LocalAliases { + if localAlias == bucketName { + bucketId = bucket.Id + *global = false + break findBucketIdLoop + } + } + for _, globalAlias := range bucket.GlobalAliases { + if globalAlias == bucketName { + bucketId = bucket.Id + *global = true + break findBucketIdLoop + } + } + } + + if bucketId == nil || global == nil { + http.Error(w, "Bucket not found in this account", http.StatusNotFound) + return nil, errors.New("Unable to fetch bucket ID") + } + + return &BucketRequest{ + s3key: s3key, + bucketName: bucketName, + bucketId: *bucketId, + global: *global, + http: r, + }, nil +} + +func patchGarageBucket(w http.ResponseWriter, br *BucketRequest) { + var err error + + // DECODE BODY + var queuedChange ApiBucketView + decoder := json.NewDecoder(br.http.Body) + err = decoder.Decode(&queuedChange) + if err != nil { + log.Println(err) + http.Error(w, "Unable to decode the body", http.StatusBadRequest) + return + } + + // SET THE GLOBAL FLAG + if queuedChange.global != nil { + if *queuedChange.global && !br.global { + _, err = grgAddGlobalAlias(br.bucketId, br.bucketName) + if err != nil { + http.Error(w, "Unable to add the requested name as global alias for this bucket", http.StatusInternalServerError) + return + } + _, err = grgDelLocalAlias(br.bucketId, *br.s3key.AccessKeyId, br.bucketName) + if err != nil { + http.Error(w, "Unable to remove the local alias for this bucket", http.StatusInternalServerError) + return + } + } else if !*queuedChange.global && br.global { + grgAddLocalAlias(br.bucketId, *br.s3key.AccessKeyId, br.bucketName) + if err != nil { + http.Error(w, "Unable to add the requested name as local alias for this bucket", http.StatusInternalServerError) + return + } + grgDelGlobalAlias(br.bucketId, br.bucketName) + if err != nil { + http.Error(w, "Unable to remove the global alias for this bucket", http.StatusInternalServerError) + return + } + } + } + + // CHECK IF QUOTA MUST BE ADDED TO THIS BUCKET + + // VALIDATE IT // --- global --- // 1. can be true, false, or nil (use pointers) // 2. if nil do nothing @@ -137,17 +249,28 @@ func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) { // --- quota.files --- // 1. if no quota on the bucket + this field is none, set to 10k // 2. if lower than 10k, set to 10k. If higher than 40k, set to 40k + // READ BODY JSON // IF BODY.GLOBAL is not NONE - // Add an alias + // DO: Add an alias // IF BODY.QUOTA.SIZE is not NONE - // Change quota + // DO: Change quota // IF BODY.QUOTA.FILE is not NONE - // Change quota + // DO: Change quota - log.Println(login, s3key) - - return + getGarageBucket(w, br) +} + +func getGarageBucket(w http.ResponseWriter, br *BucketRequest) { + // FETCH AN UPDATED BUCKET VIEW + bucket, err := grgGetBucket(br.bucketId) + if err != nil { + http.Error(w, "Unable to fetch bucket details", http.StatusInternalServerError) + return + } + + // BUILD A VIEW + log.Println(bucket) } diff --git a/garage.go b/garage.go index 61d1b93..db35366 100644 --- a/garage.go +++ b/garage.go @@ -97,12 +97,56 @@ func grgCreateWebsite(gkey, bucket string) (*garage.BucketInfo, error) { return binfo, nil } +func grgAddGlobalAlias(bid, alias string) (*garage.BucketInfo, error) { + client, ctx := gadmin() + + resp, _, err := client.BucketApi.PutBucketGlobalAlias(ctx).Id(bid).Alias(alias).Execute() + if err != nil { + log.Println(err) + return nil, err + } + return resp, nil +} + +func grgAddLocalAlias(bid, key, alias string) (*garage.BucketInfo, error) { + client, ctx := gadmin() + + resp, _, err := client.BucketApi.PutBucketLocalAlias(ctx).Id(bid).AccessKeyId(key).Alias(alias).Execute() + if err != nil { + log.Println(err) + return nil, err + } + return resp, nil +} + +func grgDelGlobalAlias(bid, alias string) (*garage.BucketInfo, error) { + client, ctx := gadmin() + + resp, _, err := client.BucketApi.DeleteBucketGlobalAlias(ctx).Id(bid).Alias(alias).Execute() + if err != nil { + log.Println(err) + return nil, err + } + return resp, nil +} + +func grgDelLocalAlias(bid, key, alias string) (*garage.BucketInfo, error) { + client, ctx := gadmin() + + resp, _, err := client.BucketApi.DeleteBucketLocalAlias(ctx).Id(bid).AccessKeyId(key).Alias(alias).Execute() + if err != nil { + log.Println(err) + return nil, err + } + return resp, nil +} + func grgGetBucket(bid string) (*garage.BucketInfo, error) { client, ctx := gadmin() resp, _, err := client.BucketApi.GetBucketInfo(ctx, bid).Execute() if err != nil { - fmt.Printf("%+v\n", err) + log.Println(err) return nil, err } return resp, nil diff --git a/main.go b/main.go index 1402ff2..8bfb2f8 100644 --- a/main.go +++ b/main.go @@ -130,7 +130,7 @@ func main() { r.HandleFunc("/", handleHome) r.HandleFunc("/logout", handleLogout) - r.HandleFunc("/api/unstable/garage/bucket/{b}", handleAPIGarageBucket) + r.HandleFunc("/api/unstable/garage/bucket/{bucket}", handleAPIGarageBucket) r.HandleFunc("/profile", handleProfile) r.HandleFunc("/passwd", handlePasswd) From d0ed765be72f80034fa530ba037e488c35abdd9e Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 25 Sep 2023 10:27:49 +0200 Subject: [PATCH 07/14] add a cli feature --- api.go | 5 ++--- cli.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 ++- go.sum | 5 ++++- main.go | 22 ++++++++++++++++++++-- 5 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 cli.go diff --git a/api.go b/api.go index 1ddb4ea..6bff15d 100644 --- a/api.go +++ b/api.go @@ -30,8 +30,7 @@ func checkLoginAPI(w http.ResponseWriter, r *http.Request) (*LoginStatus, error) l := ldapOpen(w) if l == nil { - log.Println(l) - http.Error(w, "Internal server error", http.StatusInternalServerError) + log.Println("Unable to open LDAP connection") return nil, errors.New("Unable to open LDAP connection") } @@ -154,7 +153,7 @@ func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) { func buildBucketRequest(w http.ResponseWriter, r *http.Request) (*BucketRequest, error) { _, s3key, err := checkLoginAndS3API(w, r) if err != nil { - http.Error(w, "Unable to connect on LDAP", http.StatusUnauthorized) + //http.Error(w, "Unable to connect on LDAP", http.StatusUnauthorized) return nil, err } diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..df11460 --- /dev/null +++ b/cli.go @@ -0,0 +1,44 @@ +package main + +import ( + "flag" + "fmt" + "os" + "syscall" + "golang.org/x/term" +) + +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) +} diff --git a/go.mod b/go.mod index bacf791..56bd9f6 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/minio/minio-go/v7 v7.0.0 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + golang.org/x/term v0.12.0 ) require ( @@ -29,7 +30,7 @@ require ( github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect - golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 // indirect + golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.3.3 // indirect google.golang.org/appengine v1.6.6 // indirect google.golang.org/protobuf v1.25.0 // indirect diff --git a/go.sum b/go.sum index 3e2e72d..ae748fd 100644 --- a/go.sum +++ b/go.sum @@ -278,8 +278,11 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index 8bfb2f8..c809d25 100644 --- a/main.go +++ b/main.go @@ -58,7 +58,8 @@ type ConfigFile struct { S3Bucket string `json:"s3_bucket"` } -var configFlag = flag.String("config", "./config.json", "Configuration file path") +var fsServer = flag.NewFlagSet("server", flag.ContinueOnError) +var configFlag = fsServer.String("config", "./config.json", "Configuration file path") var config *ConfigFile @@ -114,8 +115,25 @@ func getTemplate(name string) *template.Template { } func main() { - flag.Parse() + if len(os.Args) < 2 { + server(os.Args[1:]) + return + } + switch os.Args[1] { + case "cli": + cliMain(os.Args[2:]) + case "server": + server(os.Args[2:]) + default: + log.Println("Usage: guichet [server|cli] --help") + os.Exit(1) + } +} + +func server(args []string) { + log.Println("Starting Guichet Server") + fsServer.Parse(args) config_file := readConfig() config = &config_file From c06f52837e5b4aab5335e5a66885c48c24a148a2 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 25 Sep 2023 15:35:54 +0200 Subject: [PATCH 08/14] WIP refactor (broken templates) --- admin.go | 98 +++++------ api.go | 117 ++----------- directory.go | 10 +- garage.go | 100 ++++------- integration/docker-compose.yml | 8 +- invite.go | 36 ++-- login.go | 293 +++++++++++++++++++++++++++++++++ main.go | 198 +++------------------- picture.go | 10 +- profile.go | 40 ++--- quotas.go | 79 +++++++++ 11 files changed, 534 insertions(+), 455 deletions(-) create mode 100644 login.go create mode 100644 quotas.go diff --git a/admin.go b/admin.go index 18d1fb2..3c805fb 100644 --- a/admin.go +++ b/admin.go @@ -11,18 +11,18 @@ import ( "github.com/gorilla/mux" ) -func checkAdminLogin(w http.ResponseWriter, r *http.Request) *LoginStatus { - login := checkLogin(w, r) - if login == nil { +func checkAdminLogin(w http.ResponseWriter, r *http.Request) *LoggedUser { + user := RequireUserHtml(w, r) + if user == nil { return nil } - if !login.CanAdmin { + if !user.Capabilities.CanAdmin { http.Error(w, "Not authorized to perform administrative operations.", http.StatusUnauthorized) return nil } - return login + return user } type EntryList []*ldap.Entry @@ -40,7 +40,7 @@ func (d EntryList) Less(i, j int) bool { } type AdminUsersTplData struct { - Login *LoginStatus + User *LoggedUser UserNameAttr string UserBaseDN string Users EntryList @@ -49,8 +49,8 @@ type AdminUsersTplData struct { func handleAdminUsers(w http.ResponseWriter, r *http.Request) { templateAdminUsers := getTemplate("admin_users.html") - login := checkAdminLogin(w, r) - if login == nil { + user := checkAdminLogin(w, r) + if user == nil { return } @@ -61,14 +61,14 @@ func handleAdminUsers(w http.ResponseWriter, r *http.Request) { []string{config.UserNameAttr, "dn", "displayname", "givenname", "sn", "mail"}, nil) - sr, err := login.conn.Search(searchRequest) + sr, err := user.Login.conn.Search(searchRequest) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } data := &AdminUsersTplData{ - Login: login, + User: user, UserNameAttr: config.UserNameAttr, UserBaseDN: config.UserBaseDN, Users: EntryList(sr.Entries), @@ -79,7 +79,7 @@ func handleAdminUsers(w http.ResponseWriter, r *http.Request) { } type AdminGroupsTplData struct { - Login *LoginStatus + User *LoggedUser GroupNameAttr string GroupBaseDN string Groups EntryList @@ -88,8 +88,8 @@ type AdminGroupsTplData struct { func handleAdminGroups(w http.ResponseWriter, r *http.Request) { templateAdminGroups := getTemplate("admin_groups.html") - login := checkAdminLogin(w, r) - if login == nil { + user := checkAdminLogin(w, r) + if user == nil { return } @@ -100,14 +100,14 @@ func handleAdminGroups(w http.ResponseWriter, r *http.Request) { []string{config.GroupNameAttr, "dn", "description"}, nil) - sr, err := login.conn.Search(searchRequest) + sr, err := user.Login.conn.Search(searchRequest) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } data := &AdminGroupsTplData{ - Login: login, + User: user, GroupNameAttr: config.GroupNameAttr, GroupBaseDN: config.GroupBaseDN, Groups: EntryList(sr.Entries), @@ -118,7 +118,7 @@ func handleAdminGroups(w http.ResponseWriter, r *http.Request) { } type AdminMailingTplData struct { - Login *LoginStatus + User *LoggedUser MailingNameAttr string MailingBaseDN string MailingLists EntryList @@ -127,8 +127,8 @@ type AdminMailingTplData struct { func handleAdminMailing(w http.ResponseWriter, r *http.Request) { templateAdminMailing := getTemplate("admin_mailing.html") - login := checkAdminLogin(w, r) - if login == nil { + user := checkAdminLogin(w, r) + if user == nil { return } @@ -139,14 +139,14 @@ func handleAdminMailing(w http.ResponseWriter, r *http.Request) { []string{config.MailingNameAttr, "dn", "description"}, nil) - sr, err := login.conn.Search(searchRequest) + sr, err := user.Login.conn.Search(searchRequest) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } data := &AdminMailingTplData{ - Login: login, + User: user, MailingNameAttr: config.MailingNameAttr, MailingBaseDN: config.MailingBaseDN, MailingLists: EntryList(sr.Entries), @@ -157,7 +157,7 @@ func handleAdminMailing(w http.ResponseWriter, r *http.Request) { } type AdminMailingListTplData struct { - Login *LoginStatus + User *LoggedUser MailingNameAttr string MailingBaseDN string @@ -173,8 +173,8 @@ type AdminMailingListTplData struct { func handleAdminMailingList(w http.ResponseWriter, r *http.Request) { templateAdminMailingList := getTemplate("admin_mailing_list.html") - login := checkAdminLogin(w, r) - if login == nil { + user := checkAdminLogin(w, r) + if user == nil { return } @@ -193,7 +193,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) { modify_request := ldap.NewModifyRequest(dn, nil) modify_request.Add("member", []string{member}) - err := login.conn.Modify(modify_request) + err := user.Login.conn.Modify(modify_request) if err != nil { dError = err.Error() } else { @@ -209,7 +209,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) { fmt.Sprintf("(&(objectClass=organizationalPerson)(mail=%s))", mail), []string{"dn", "displayname", "mail"}, nil) - sr, err := login.conn.Search(searchRequest) + sr, err := user.Login.conn.Search(searchRequest) if err != nil { dError = err.Error() } else { @@ -222,14 +222,14 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) { if displayname != "" { req.Attribute("displayname", []string{displayname}) } - err := login.conn.Add(req) + err := user.Login.conn.Add(req) if err != nil { dError = err.Error() } else { modify_request := ldap.NewModifyRequest(dn, nil) modify_request.Add("member", []string{guestDn}) - err := login.conn.Modify(modify_request) + err := user.Login.conn.Modify(modify_request) if err != nil { dError = err.Error() } else { @@ -243,7 +243,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) { modify_request := ldap.NewModifyRequest(dn, nil) modify_request.Add("member", []string{sr.Entries[0].DN}) - err := login.conn.Modify(modify_request) + err := user.Login.conn.Modify(modify_request) if err != nil { dError = err.Error() } else { @@ -258,7 +258,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) { modify_request := ldap.NewModifyRequest(dn, nil) modify_request.Delete("member", []string{member}) - err := login.conn.Modify(modify_request) + err := user.Login.conn.Modify(modify_request) if err != nil { dError = err.Error() } else { @@ -275,7 +275,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) { []string{"dn", config.MailingNameAttr, "member", "description"}, nil) - sr, err := login.conn.Search(searchRequest) + sr, err := user.Login.conn.Search(searchRequest) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -307,7 +307,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) { fmt.Sprintf("(objectClass=organizationalPerson)"), []string{"dn", "displayname", "mail"}, nil) - sr, err = login.conn.Search(searchRequest) + sr, err = user.Login.conn.Search(searchRequest) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -322,7 +322,7 @@ func handleAdminMailingList(w http.ResponseWriter, r *http.Request) { } data := &AdminMailingListTplData{ - Login: login, + User: user, MailingNameAttr: config.MailingNameAttr, MailingBaseDN: config.MailingBaseDN, @@ -394,8 +394,8 @@ type PropValues struct { func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { templateAdminLDAP := getTemplate("admin_ldap.html") - login := checkAdminLogin(w, r) - if login == nil { + user := checkAdminLogin(w, r) + if user == nil { return } @@ -445,7 +445,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { modify_request := ldap.NewModifyRequest(dn, nil) modify_request.Replace(attr, values_filtered) - err := login.conn.Modify(modify_request) + err := user.Login.conn.Modify(modify_request) if err != nil { dError = err.Error() } else { @@ -466,7 +466,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { modify_request := ldap.NewModifyRequest(dn, nil) modify_request.Add(attr, values_filtered) - err := login.conn.Modify(modify_request) + err := user.Login.conn.Modify(modify_request) if err != nil { dError = err.Error() } else { @@ -478,7 +478,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { modify_request := ldap.NewModifyRequest(dn, nil) modify_request.Replace(attr, []string{}) - err := login.conn.Modify(modify_request) + err := user.Login.conn.Modify(modify_request) if err != nil { dError = err.Error() } else { @@ -489,7 +489,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { modify_request := ldap.NewModifyRequest(group, nil) modify_request.Delete("member", []string{dn}) - err := login.conn.Modify(modify_request) + err := user.Login.conn.Modify(modify_request) if err != nil { dError = err.Error() } else { @@ -500,7 +500,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { modify_request := ldap.NewModifyRequest(group, nil) modify_request.Add("member", []string{dn}) - err := login.conn.Modify(modify_request) + err := user.Login.conn.Modify(modify_request) if err != nil { dError = err.Error() } else { @@ -511,7 +511,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { modify_request := ldap.NewModifyRequest(dn, nil) modify_request.Delete("member", []string{member}) - err := login.conn.Modify(modify_request) + err := user.Login.conn.Modify(modify_request) if err != nil { dError = err.Error() } else { @@ -519,7 +519,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { } } else if action == "delete-object" { del_request := ldap.NewDelRequest(dn, nil) - err := login.conn.Del(del_request) + err := user.Login.conn.Del(del_request) if err != nil { dError = err.Error() } else { @@ -537,7 +537,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { []string{}, nil) - sr, err := login.conn.Search(searchRequest) + sr, err := user.Login.conn.Search(searchRequest) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -621,7 +621,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { fmt.Sprintf("(objectClass=organizationalPerson)"), []string{"dn", "displayname", "description"}, nil) - sr, err = login.conn.Search(searchRequest) + sr, err = user.Login.conn.Search(searchRequest) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -675,7 +675,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { fmt.Sprintf("(objectClass=groupOfNames)"), []string{"dn", "description"}, nil) - sr, err = login.conn.Search(searchRequest) + sr, err = user.Login.conn.Search(searchRequest) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -719,7 +719,7 @@ func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { []string{"dn", "displayname", "description"}, nil) - sr, err = login.conn.Search(searchRequest) + sr, err = user.Login.conn.Search(searchRequest) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -787,8 +787,8 @@ type CreateData struct { func handleAdminCreate(w http.ResponseWriter, r *http.Request) { templateAdminCreate := getTemplate("admin_create.html") - login := checkAdminLogin(w, r) - if login == nil { + user := checkAdminLogin(w, r) + if user == nil { return } @@ -803,7 +803,7 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) { []string{}, nil) - sr, err := login.conn.Search(searchRequest) + sr, err := user.Login.conn.Search(searchRequest) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -894,7 +894,7 @@ func handleAdminCreate(w http.ResponseWriter, r *http.Request) { req.Attribute("description", []string{data.Description}) } - err := login.conn.Add(req) + err := user.Login.conn.Add(req) if err != nil { data.Error = err.Error() } else { diff --git a/api.go b/api.go index 6bff15d..7d9c2cd 100644 --- a/api.go +++ b/api.go @@ -2,115 +2,14 @@ package main import ( //"context" - "encoding/json" "errors" - "fmt" + "encoding/json" garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" - "github.com/go-ldap/ldap/v3" "github.com/gorilla/mux" "log" "net/http" - "strings" ) -func checkLoginAPI(w http.ResponseWriter, r *http.Request) (*LoginStatus, error) { - username, password, ok := r.BasicAuth() - if !ok { - w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return nil, errors.New("Missing or invalid 'Authenticate: Basic' field") - } - user_dn := buildUserDN(username) - - login_info := &LoginInfo{ - DN: user_dn, - Username: username, - Password: password, - } - - l := ldapOpen(w) - if l == nil { - log.Println("Unable to open LDAP connection") - return nil, errors.New("Unable to open LDAP connection") - } - - err := l.Bind(login_info.DN, login_info.Password) - if err != nil { - w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return nil, errors.New("Unable to bind this user+password combination on the LDAP server") - } - - loginStatus := &LoginStatus{ - Info: login_info, - conn: l, - } - - requestKind := "(objectClass=organizationalPerson)" - - if strings.EqualFold(login_info.DN, config.AdminAccount) { - requestKind = "(objectclass=*)" - } - searchRequest := ldap.NewSearchRequest( - login_info.DN, - ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, - requestKind, - []string{ - "dn", - "displayname", - "givenname", - "sn", - "mail", - "memberof", - "description", - "garage_s3_access_key", - FIELD_NAME_DIRECTORY_VISIBILITY, - FIELD_NAME_PROFILE_PICTURE, - }, - nil) - - sr, err := l.Search(searchRequest) - if err != nil { - log.Println(err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return nil, errors.New("Unable to search essential information about the logged user on LDAP") - } - - if len(sr.Entries) != 1 { - log.Println(fmt.Sprintf("Unable to find entry for %s", login_info.DN)) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return nil, errors.New("Not enough or too many entries for this user in the LDAP directory (expect a unique result)") - } - - loginStatus.UserEntry = sr.Entries[0] - - loginStatus.CanAdmin = strings.EqualFold(loginStatus.Info.DN, config.AdminAccount) - loginStatus.CanInvite = false - for _, attr := range loginStatus.UserEntry.Attributes { - if strings.EqualFold(attr.Name, "memberof") { - for _, group := range attr.Values { - if config.GroupCanInvite != "" && strings.EqualFold(group, config.GroupCanInvite) { - loginStatus.CanInvite = true - } - if config.GroupCanAdmin != "" && strings.EqualFold(group, config.GroupCanAdmin) { - loginStatus.CanAdmin = true - } - } - } - } - - return loginStatus, nil -} - -func checkLoginAndS3API(w http.ResponseWriter, r *http.Request) (*LoginStatus, *garage.KeyInfo, error) { - login, err := checkLoginAPI(w, r) - if err != nil { - return nil, nil, err - } - keyPair, err := checkS3(login) - return login, keyPair, err -} - type ApiQuotaView struct { files *uint64 size *uint64 @@ -131,6 +30,7 @@ type BucketRequest struct { } func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) { + br, err := buildBucketRequest(w, r) if err != nil { return @@ -151,10 +51,9 @@ func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) { } func buildBucketRequest(w http.ResponseWriter, r *http.Request) (*BucketRequest, error) { - _, s3key, err := checkLoginAndS3API(w, r) - if err != nil { - //http.Error(w, "Unable to connect on LDAP", http.StatusUnauthorized) - return nil, err + user := RequireUserApi(w, r) + if user == nil { + return nil, errors.New("Unable to fetch user") } // FETCH BUCKET ID by iterating over buckets owned by this key @@ -162,6 +61,11 @@ func buildBucketRequest(w http.ResponseWriter, r *http.Request) (*BucketRequest, var bucketId *string var global *bool + s3key, err := user.S3KeyInfo() + if err != nil { + return nil, err + } + findBucketIdLoop: for _, bucket := range s3key.Buckets { for _, localAlias := range bucket.LocalAliases { @@ -192,6 +96,7 @@ findBucketIdLoop: global: *global, http: r, }, nil + } func patchGarageBucket(w http.ResponseWriter, br *BucketRequest) { diff --git a/directory.go b/directory.go index 0b5acd5..c7520f9 100644 --- a/directory.go +++ b/directory.go @@ -15,8 +15,8 @@ const FIELD_NAME_DIRECTORY_VISIBILITY = "directoryVisibility" func handleDirectory(w http.ResponseWriter, r *http.Request) { templateDirectory := getTemplate("directory.html") - login := checkLogin(w, r) - if login == nil { + user := RequireUserHtml(w, r) + if user == nil { return } @@ -49,8 +49,8 @@ func handleDirectorySearch(w http.ResponseWriter, r *http.Request) { } //Log to allow the research - login := checkLogin(w, r) - if login == nil { + user := RequireUserHtml(w, r) + if user == nil { http.Error(w, "Login required", http.StatusUnauthorized) return } @@ -69,7 +69,7 @@ func handleDirectorySearch(w http.ResponseWriter, r *http.Request) { }, nil) - sr, err := login.conn.Search(searchRequest) + sr, err := user.Login.conn.Search(searchRequest) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/garage.go b/garage.go index db35366..4586e26 100644 --- a/garage.go +++ b/garage.go @@ -2,16 +2,15 @@ package main import ( "context" - "errors" "fmt" garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" - "github.com/go-ldap/ldap/v3" "github.com/gorilla/mux" "log" "net/http" "strings" ) + func gadmin() (*garage.APIClient, context.Context) { // Set Host and other parameters configuration := garage.NewConfiguration() @@ -48,7 +47,9 @@ func grgGetKey(accessKey string) (*garage.KeyInfo, error) { return resp, nil } -func grgCreateWebsite(gkey, bucket string) (*garage.BucketInfo, error) { + + +func grgCreateWebsite(gkey, bucket string, quotas *UserQuota) (*garage.BucketInfo, error) { client, ctx := gadmin() br := garage.NewCreateBucketRequest() @@ -79,9 +80,7 @@ func grgCreateWebsite(gkey, bucket string) (*garage.BucketInfo, error) { wr.SetIndexDocument("index.html") wr.SetErrorDocument("error.html") - qr := garage.NewUpdateBucketRequestQuotas() - qr.SetMaxSize(1024 * 1024 * 50) // 50MB - qr.SetMaxObjects(10000) //10k objects + qr := quotas.DefaultWebsiteQuota() ur := garage.NewUpdateBucketRequest() ur.SetWebsiteAccess(*wr) @@ -153,85 +152,37 @@ func grgGetBucket(bid string) (*garage.BucketInfo, error) { } -func checkS3(login *LoginStatus) (*garage.KeyInfo, error) { - if login == nil { - return nil, errors.New("Login can't be nil") - } - keyID := login.UserEntry.GetAttributeValue("garage_s3_access_key") - if keyID == "" { - keyPair, err := grgCreateKey(login.Info.Username) - if err != nil { - return nil, err - } - modify_request := ldap.NewModifyRequest(login.Info.DN, nil) - modify_request.Replace("garage_s3_access_key", []string{*keyPair.AccessKeyId}) - // @FIXME compatibility feature for bagage (SFTP+webdav) - // you can remove it once bagage will be updated to fetch the key from garage directly - // or when bottin will be able to dynamically fetch it. - modify_request.Replace("garage_s3_secret_key", []string{*keyPair.SecretAccessKey}) - err = login.conn.Modify(modify_request) - return keyPair, err - } - // Note: we could simply return the login info, but LX asked we do not - // store the secrets in LDAP in the future. - keyPair, err := grgGetKey(keyID) - return keyPair, err -} - -func checkLoginAndS3(w http.ResponseWriter, r *http.Request) (*LoginStatus, *garage.KeyInfo, error) { - login := checkLogin(w, r) - if login == nil { - return nil, nil, errors.New("LDAP login failed") - } - keyPair, err := checkS3(login) - return login, keyPair, err -} - -type keyView struct { - Status *LoginStatus - Key *garage.KeyInfo -} +// --- Start page rendering functions func handleGarageKey(w http.ResponseWriter, r *http.Request) { - login, s3key, err := checkLoginAndS3(w, r) - if err != nil { - log.Println(err) + user := RequireUserHtml(w, r) + if user == nil { return } - view := keyView{Status: login, Key: s3key} tKey := getTemplate("garage_key.html") - tKey.Execute(w, &view) -} - -type webListView struct { - Status *LoginStatus - Key *garage.KeyInfo + tKey.Execute(w, user) } func handleGarageWebsiteList(w http.ResponseWriter, r *http.Request) { - login, s3key, err := checkLoginAndS3(w, r) - if err != nil { - log.Println(err) + user := RequireUserHtml(w, r) + if user == nil { return } - view := webListView{Status: login, Key: s3key} tWebsiteList := getTemplate("garage_website_list.html") - tWebsiteList.Execute(w, &view) + tWebsiteList.Execute(w, user) } func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) { - _, s3key, err := checkLoginAndS3(w, r) - if err != nil { - log.Println(err) + user := RequireUserHtml(w, r) + if user == nil { return } tWebsiteNew := getTemplate("garage_website_new.html") if r.Method == "POST" { r.ParseForm() - log.Println(r.Form) bucket := strings.Join(r.Form["bucket"], "") if bucket == "" { @@ -244,7 +195,15 @@ func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) { return } - binfo, err := grgCreateWebsite(*s3key.AccessKeyId, bucket) + keyInfo, err := user.S3KeyInfo() + if err != nil { + log.Println(err) + // @FIXME we need to return the error to the user + tWebsiteNew.Execute(w, nil) + return + } + + binfo, err := grgCreateWebsite(*keyInfo.AccessKeyId, bucket, user.Quota) if err != nil { log.Println(err) // @FIXME we need to return the error to the user @@ -260,8 +219,7 @@ func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) { } type webInspectView struct { - Status *LoginStatus - Key *garage.KeyInfo + User *LoggedUser Bucket *garage.BucketInfo IndexDoc string ErrorDoc string @@ -271,13 +229,14 @@ type webInspectView struct { } func handleGarageWebsiteInspect(w http.ResponseWriter, r *http.Request) { - login, s3key, err := checkLoginAndS3(w, r) - if err != nil { - log.Println(err) + user := RequireUserHtml(w, r) + if user == nil { return } bucketId := mux.Vars(r)["bucket"] + // @FIXME check that user owns the bucket.... + binfo, err := grgGetBucket(bucketId) if err != nil { log.Println(err) @@ -288,8 +247,7 @@ func handleGarageWebsiteInspect(w http.ResponseWriter, r *http.Request) { q := binfo.GetQuotas() view := webInspectView{ - Status: login, - Key: s3key, + User: user, Bucket: binfo, IndexDoc: (&wc).GetIndexDocument(), ErrorDoc: (&wc).GetErrorDocument(), diff --git a/integration/docker-compose.yml b/integration/docker-compose.yml index cf1c088..ec855db 100644 --- a/integration/docker-compose.yml +++ b/integration/docker-compose.yml @@ -1,19 +1,19 @@ version: '3' services: consul: - image: consul + image: hashicorp/consul:1.16 restart: "always" expose: - 8500 bottin: - image: dxflrs/bottin:dnp41vp8w24h4mbh0xg1mybzr1f46k41 - command: "-config /etc/bottin.json" + image: dxflrs/bottin:7h18i30cckckaahv87d3c86pn4a7q41z + #command: "-config /etc/bottin.json" restart: "always" depends_on: ["consul"] ports: - "389:389" volumes: - - "./config/bottin.json:/etc/bottin.json" + - "./config/bottin.json:/config.json" garage: image: dxflrs/garage:v0.8.2 ports: diff --git a/invite.go b/invite.go index 1384d70..0a0e836 100644 --- a/invite.go +++ b/invite.go @@ -21,29 +21,29 @@ import ( var EMAIL_REGEXP = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") -func checkInviterLogin(w http.ResponseWriter, r *http.Request) *LoginStatus { - login := checkLogin(w, r) - if login == nil { +func checkInviterLogin(w http.ResponseWriter, r *http.Request) *LoggedUser { + user := RequireUserHtml(w, r) + if user == nil { return nil } - if !login.CanInvite { + if !user.Capabilities.CanInvite { http.Error(w, "Not authorized to invite new users.", http.StatusUnauthorized) return nil } - return login + return user } // New account creation directly from interface func handleInviteNewAccount(w http.ResponseWriter, r *http.Request) { - login := checkInviterLogin(w, r) - if login == nil { + user := checkInviterLogin(w, r) + if user == nil { return } - handleNewAccount(w, r, login.conn, login.Info.DN) + handleNewAccount(w, r, user.Login.conn, user.Login.Info.DN()) } // New account creation using code @@ -52,13 +52,13 @@ func handleInvitationCode(w http.ResponseWriter, r *http.Request) { code := mux.Vars(r)["code"] code_id, code_pw := readCode(code) - l := ldapOpen(w) - if l == nil { - return + l, err := NewLdapCon() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) } inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN - err := l.Bind(inviteDn, code_pw) + err = l.Bind(inviteDn, code_pw) if err != nil { templateInviteInvalidCode := getTemplate("invite_invalid_code.html") templateInviteInvalidCode.Execute(w, nil) @@ -241,8 +241,8 @@ type CodeMailFields struct { func handleInviteSendCode(w http.ResponseWriter, r *http.Request) { templateInviteSendCode := getTemplate("invite_send_code.html") - login := checkInviterLogin(w, r) - if login == nil { + user := checkInviterLogin(w, r) + if user == nil { return } @@ -257,14 +257,14 @@ func handleInviteSendCode(w http.ResponseWriter, r *http.Request) { sendto := strings.Join(r.Form["sendto"], "") if choice == "display" || choice == "send" { - trySendCode(login, choice, sendto, data) + trySendCode(user, choice, sendto, data) } } templateInviteSendCode.Execute(w, data) } -func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCodeData) { +func trySendCode(user *LoggedUser, choice string, sendto string, data *SendCodeData) { // Generate code code, code_id, code_pw := genCode() @@ -279,7 +279,7 @@ func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCod req.Attribute("userpassword", []string{pw}) req.Attribute("objectclass", []string{"top", "invitationCode"}) - err = login.conn.Add(req) + err = user.Login.conn.Add(req) if err != nil { data.ErrorMessage = err.Error() return @@ -303,7 +303,7 @@ func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCod templateMail.Execute(buf, &CodeMailFields{ To: sendto, From: config.MailFrom, - InviteFrom: login.WelcomeName(), + InviteFrom: user.WelcomeName(), Code: code, WebBaseAddress: config.WebAddress, }) diff --git a/login.go b/login.go new file mode 100644 index 0000000..87b7a67 --- /dev/null +++ b/login.go @@ -0,0 +1,293 @@ +package main + +import ( + "crypto/tls" + "errors" + "fmt" + "net/http" + "strings" + + garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" + "github.com/go-ldap/ldap/v3" +) + +var ( + ErrNotAuthenticatedSession = fmt.Errorf("User has no session") + ErrNotAuthenticatedBasic = fmt.Errorf("User has not sent Authentication Basic information") + ErrNotAuthenticated = fmt.Errorf("User is not authenticated") + ErrWrongLDAPCredentials = fmt.Errorf("LDAP credentials are wrong") + ErrLDAPServerUnreachable = fmt.Errorf("Unable to open the LDAP server") + ErrLDAPSearchInternalError = fmt.Errorf("LDAP Search of this user failed with an internal error") + ErrLDAPSearchNotFound = fmt.Errorf("User is authenticated but its associated data can not be found during search") +) + +// --- Login Info --- +type LoginInfo struct { + Username string + Password string +} + +func NewLoginInfoFromSession(r *http.Request) (*LoginInfo, error) { + session, err := store.Get(r, SESSION_NAME) + if err == nil { + username, ok_user := session.Values["login_username"] + password, ok_pwd := session.Values["login_password"] + + if ok_user && ok_pwd { + loginInfo := &LoginInfo{ + Username: username.(string), + Password: password.(string), + } + return loginInfo, nil + } + } + + return nil, errors.Join(ErrNotAuthenticatedSession, err) +} + +func NewLoginInfoFromBasicAuth(r *http.Request) (*LoginInfo, error) { + username, password, ok := r.BasicAuth() + if ok { + login_info := &LoginInfo{ + Username: username, + Password: password, + } + + return login_info, nil + } + return nil, ErrNotAuthenticatedBasic +} + +func (li *LoginInfo) DN() string { + user_dn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, li.Username, config.UserBaseDN) + if strings.EqualFold(li.Username, config.AdminAccount) { + user_dn = li.Username + } + + return user_dn +} + +// --- Login Status --- +type LoginStatus struct { + Info *LoginInfo + conn *ldap.Conn +} + +func NewLoginStatus(r *http.Request, login_info *LoginInfo) (*LoginStatus, error) { + l, err := NewLdapCon() + if err != nil { + return nil, err + } + + err = l.Bind(login_info.DN(), login_info.Password) + if err != nil { + return nil, errors.Join(ErrWrongLDAPCredentials, err) + } + + loginStatus := &LoginStatus{ + Info: login_info, + conn: l, + } + return loginStatus, nil +} + +func NewLdapCon() (*ldap.Conn, error) { + l, err := ldap.DialURL(config.LdapServerAddr) + if err != nil { + return nil, errors.Join(ErrLDAPServerUnreachable, err) + } + + if config.LdapTLS { + err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) + if err != nil { + return nil, errors.Join(ErrLDAPServerUnreachable, err) + } + } + + return l, nil +} + +// --- Capabilities --- +type Capabilities struct { + CanAdmin bool + CanInvite bool +} +func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities { + // Initialize + canAdmin := false + canInvite := false + + // Special case for the "admin" account that is de-facto admin + canAdmin = strings.EqualFold(login.Info.DN(), config.AdminAccount) + + // Check if this account is part of a group that give capabilities + for _, attr := range entry.Attributes { + if strings.EqualFold(attr.Name, "memberof") { + for _, group := range attr.Values { + if config.GroupCanInvite != "" && strings.EqualFold(group, config.GroupCanInvite) { + canInvite = true + } + if config.GroupCanAdmin != "" && strings.EqualFold(group, config.GroupCanAdmin) { + canAdmin = true + } + } + } + } + + return &Capabilities{ + CanAdmin: canAdmin, + CanInvite: canInvite, + } +} + +// --- Logged User --- +type LoggedUser struct { + Login *LoginStatus + Entry *ldap.Entry + Capabilities *Capabilities + Quota *UserQuota + s3key *garage.KeyInfo +} +func NewLoggedUser(login *LoginStatus) (*LoggedUser, error) { + requestKind := "(objectClass=organizationalPerson)" + if strings.EqualFold(login.Info.DN(), config.AdminAccount) { + requestKind = "(objectclass=*)" + } + + searchRequest := ldap.NewSearchRequest( + login.Info.DN(), + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, + requestKind, + []string{ + "dn", + "displayname", + "givenname", + "sn", + "mail", + "memberof", + "description", + "garage_s3_access_key", + FIELD_NAME_DIRECTORY_VISIBILITY, + FIELD_NAME_PROFILE_PICTURE, + FIELD_QUOTA_WEBSITE_SIZE_BURSTED, + FIELD_QUOTA_WEBSITE_COUNT, + }, + nil) + + sr, err := login.conn.Search(searchRequest) + if err != nil { + return nil, ErrLDAPSearchInternalError + } + + if len(sr.Entries) != 1 { + return nil, ErrLDAPSearchNotFound + } + entry := sr.Entries[0] + + lu := &LoggedUser { + Login: login, + Entry: entry, + Capabilities: NewCapabilities(login, entry), + Quota: NewUserQuotaFromEntry(entry), + } + return lu, nil +} +func (lu *LoggedUser) WelcomeName() string { + ret := lu.Entry.GetAttributeValue("givenname") + if ret == "" { + ret = lu.Entry.GetAttributeValue("displayname") + } + if ret == "" { + ret = lu.Login.Info.Username + } + return ret +} +func (lu *LoggedUser) S3KeyInfo() (*garage.KeyInfo, error) { + var err error + var keyPair *garage.KeyInfo + + if lu.s3key == nil { + keyID := lu.Entry.GetAttributeValue("garage_s3_access_key") + if keyID == "" { + // If there is no S3Key in LDAP, generate it... + keyPair, err = grgCreateKey(lu.Login.Info.Username) + if err != nil { + return nil, err + } + modify_request := ldap.NewModifyRequest(lu.Login.Info.DN(), nil) + modify_request.Replace("garage_s3_access_key", []string{*keyPair.AccessKeyId}) + // @FIXME compatibility feature for bagage (SFTP+webdav) + // you can remove it once bagage will be updated to fetch the key from garage directly + // or when bottin will be able to dynamically fetch it. + modify_request.Replace("garage_s3_secret_key", []string{*keyPair.SecretAccessKey}) + err = lu.Login.conn.Modify(modify_request) + if err != nil { + return nil, err + } + } else { + // There is an S3 key in LDAP, fetch its descriptor... + keyPair, err = grgGetKey(keyID) + if err != nil { + return nil, err + } + } + + // Cache the keypair... + lu.s3key = keyPair + } + + return lu.s3key, nil +} + +// --- Require User Check +func RequireUser(r *http.Request) (*LoggedUser, error) { + var login_info *LoginInfo + + if li, err := NewLoginInfoFromSession(r); err == nil { + login_info = li + } else if li, err := NewLoginInfoFromBasicAuth(r); err == nil { + login_info = li + } else { + return nil, ErrNotAuthenticated + } + + + loginStatus, err := NewLoginStatus(r, login_info) + if err != nil { + return nil, err + } + + return NewLoggedUser(loginStatus) +} + +func RequireUserHtml(w http.ResponseWriter, r *http.Request) *LoggedUser { + user, err := RequireUser(r) + + if errors.Is(err, ErrNotAuthenticated) || errors.Is(err, ErrWrongLDAPCredentials) { + http.Redirect(w, r, "/login", http.StatusFound) + return nil + } + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return nil + } + + return user +} + +func RequireUserApi(w http.ResponseWriter, r *http.Request) *LoggedUser { + user, err := RequireUser(r) + + if errors.Is(err, ErrNotAuthenticated) || errors.Is(err, ErrWrongLDAPCredentials) { + http.Error(w, err.Error(), http.StatusUnauthorized) + return nil + } + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return nil + } + + return user +} diff --git a/main.go b/main.go index c809d25..ee1863c 100644 --- a/main.go +++ b/main.go @@ -2,10 +2,8 @@ package main import ( "crypto/rand" - "crypto/tls" "encoding/json" "flag" - "fmt" "html/template" "io/ioutil" "log" @@ -146,6 +144,7 @@ func server(args []string) { r := mux.NewRouter() r.HandleFunc("/", handleHome) + r.HandleFunc("/login", handleLogin) r.HandleFunc("/logout", handleLogout) r.HandleFunc("/api/unstable/garage/bucket/{bucket}", handleAPIGarageBucket) @@ -183,31 +182,6 @@ func server(args []string) { } } -type LoginInfo struct { - Username string - DN string - Password string -} - -type LoginStatus struct { - Info *LoginInfo - conn *ldap.Conn - UserEntry *ldap.Entry - CanAdmin bool - CanInvite bool -} - -func (login *LoginStatus) WelcomeName() string { - ret := login.UserEntry.GetAttributeValue("givenname") - if ret == "" { - ret = login.UserEntry.GetAttributeValue("displayname") - } - if ret == "" { - ret = login.Info.Username - } - return ret -} - func logRequest(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) @@ -215,149 +189,32 @@ func logRequest(handler http.Handler) http.Handler { }) } -func checkLogin(w http.ResponseWriter, r *http.Request) *LoginStatus { - var login_info *LoginInfo - - session, err := store.Get(r, SESSION_NAME) - if err == nil { - username, ok := session.Values["login_username"] - password, ok2 := session.Values["login_password"] - user_dn, ok3 := session.Values["login_dn"] - - if ok && ok2 && ok3 { - login_info = &LoginInfo{ - DN: user_dn.(string), - Username: username.(string), - Password: password.(string), - } - } - } - - if login_info == nil { - login_info = handleLogin(w, r) - if login_info == nil { - return nil - } - } - - l := ldapOpen(w) - if l == nil { - return nil - } - - err = l.Bind(login_info.DN, login_info.Password) - if err != nil { - delete(session.Values, "login_username") - delete(session.Values, "login_password") - delete(session.Values, "login_dn") - - err = session.Save(r, w) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return nil - } - return checkLogin(w, r) - } - - loginStatus := &LoginStatus{ - Info: login_info, - conn: l, - } - - requestKind := "(objectClass=organizationalPerson)" - if strings.EqualFold(login_info.DN, config.AdminAccount) { - requestKind = "(objectclass=*)" - } - searchRequest := ldap.NewSearchRequest( - login_info.DN, - ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, - requestKind, - []string{ - "dn", - "displayname", - "givenname", - "sn", - "mail", - "memberof", - "description", - "garage_s3_access_key", - FIELD_NAME_DIRECTORY_VISIBILITY, - FIELD_NAME_PROFILE_PICTURE, - }, - nil) - - sr, err := l.Search(searchRequest) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return nil - } - - if len(sr.Entries) != 1 { - http.Error(w, fmt.Sprintf("Unable to find entry for %s", login_info.DN), http.StatusInternalServerError) - return nil - } - - loginStatus.UserEntry = sr.Entries[0] - - loginStatus.CanAdmin = strings.EqualFold(loginStatus.Info.DN, config.AdminAccount) - loginStatus.CanInvite = false - for _, attr := range loginStatus.UserEntry.Attributes { - if strings.EqualFold(attr.Name, "memberof") { - for _, group := range attr.Values { - if config.GroupCanInvite != "" && strings.EqualFold(group, config.GroupCanInvite) { - loginStatus.CanInvite = true - } - if config.GroupCanAdmin != "" && strings.EqualFold(group, config.GroupCanAdmin) { - loginStatus.CanAdmin = true - } - } - } - } - - return loginStatus -} - -func ldapOpen(w http.ResponseWriter) *ldap.Conn { - l, err := ldap.DialURL(config.LdapServerAddr) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return nil - } - - if config.LdapTLS { - err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return nil - } - } - - return l -} - // Page handlers ---- + +// --- Home Controller type HomePageData struct { - Login *LoginStatus + User *LoggedUser BaseDN string } func handleHome(w http.ResponseWriter, r *http.Request) { templateHome := getTemplate("home.html") - login := checkLogin(w, r) - if login == nil { + user := RequireUserHtml(w, r) + if user == nil { return } data := &HomePageData{ - Login: login, + User: user, BaseDN: config.BaseDN, } templateHome.Execute(w, data) } +// --- Logout Controller func handleLogout(w http.ResponseWriter, r *http.Request) { session, err := store.Get(r, SESSION_NAME) if err != nil { @@ -374,9 +231,10 @@ func handleLogout(w http.ResponseWriter, r *http.Request) { return } - http.Redirect(w, r, "/", http.StatusFound) + http.Redirect(w, r, "/login", http.StatusFound) } +// --- Login Controller --- type LoginFormData struct { Username string WrongUser bool @@ -384,34 +242,26 @@ type LoginFormData struct { ErrorMessage string } -func buildUserDN(username string) string { - user_dn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, username, config.UserBaseDN) - if strings.EqualFold(username, config.AdminAccount) { - user_dn = username - } - - return user_dn -} - -func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo { +func handleLogin(w http.ResponseWriter, r *http.Request) { templateLogin := getTemplate("login.html") if r.Method == "GET" { templateLogin.Execute(w, LoginFormData{}) - return nil + return } else if r.Method == "POST" { r.ParseForm() username := strings.Join(r.Form["username"], "") password := strings.Join(r.Form["password"], "") - user_dn := buildUserDN(username) + loginInfo := LoginInfo { username, password } - l := ldapOpen(w) - if l == nil { - return nil + l, err := NewLdapCon() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return } - err := l.Bind(user_dn, password) + err = l.Bind(loginInfo.DN(), loginInfo.Password) if err != nil { data := &LoginFormData{ Username: username, @@ -424,7 +274,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo { data.ErrorMessage = err.Error() } templateLogin.Execute(w, data) - return nil + return } // Successfully logged in, save it to session @@ -435,21 +285,15 @@ func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo { session.Values["login_username"] = username session.Values["login_password"] = password - session.Values["login_dn"] = user_dn err = session.Save(r, w) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return nil + return } - return &LoginInfo{ - DN: user_dn, - Username: username, - Password: password, - } + http.Redirect(w, r, "/", http.StatusFound) } else { http.Error(w, "Unsupported method", http.StatusBadRequest) - return nil } } diff --git a/picture.go b/picture.go index 877ba05..005230d 100644 --- a/picture.go +++ b/picture.go @@ -44,7 +44,7 @@ func newMinioClient() (*minio.Client, error) { } // Upload image through guichet server. -func uploadProfilePicture(w http.ResponseWriter, r *http.Request, login *LoginStatus) (string, error) { +func uploadProfilePicture(w http.ResponseWriter, r *http.Request, user *LoggedUser) (string, error) { file, _, err := r.FormFile("image") if err == http.ErrMissingFile { @@ -74,7 +74,7 @@ func uploadProfilePicture(w http.ResponseWriter, r *http.Request, login *LoginSt // If a previous profile picture existed, delete it // (don't care about errors) - if nameConsul := login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE); nameConsul != "" { + if nameConsul := user.Entry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE); nameConsul != "" { mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul, minio.RemoveObjectOptions{}) mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul+"-thumb", minio.RemoveObjectOptions{}) } @@ -144,9 +144,9 @@ func resizePicture(file multipart.File, buffFull, buffThumb *bytes.Buffer) error func handleDownloadPicture(w http.ResponseWriter, r *http.Request) { name := mux.Vars(r)["name"] - //Check login - login := checkLogin(w, r) - if login == nil { + // Get user + user := RequireUserHtml(w, r) + if user == nil { return } diff --git a/profile.go b/profile.go index a082ad8..58e7d96 100644 --- a/profile.go +++ b/profile.go @@ -8,7 +8,7 @@ import ( ) type ProfileTplData struct { - Status *LoginStatus + User *LoggedUser ErrorMessage string Success bool Mail string @@ -23,24 +23,24 @@ type ProfileTplData struct { func handleProfile(w http.ResponseWriter, r *http.Request) { templateProfile := getTemplate("profile.html") - login := checkLogin(w, r) - if login == nil { + user := RequireUserHtml(w, r) + if user == nil { return } data := &ProfileTplData{ - Status: login, + User: user, ErrorMessage: "", Success: false, } - data.Mail = login.UserEntry.GetAttributeValue("mail") - data.DisplayName = login.UserEntry.GetAttributeValue("displayname") - data.GivenName = login.UserEntry.GetAttributeValue("givenname") - data.Surname = login.UserEntry.GetAttributeValue("sn") - data.Visibility = login.UserEntry.GetAttributeValue(FIELD_NAME_DIRECTORY_VISIBILITY) - data.Description = login.UserEntry.GetAttributeValue("description") - data.ProfilePicture = login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE) + data.Mail = user.Entry.GetAttributeValue("mail") + data.DisplayName = user.Entry.GetAttributeValue("displayname") + data.GivenName = user.Entry.GetAttributeValue("givenname") + data.Surname = user.Entry.GetAttributeValue("sn") + data.Visibility = user.Entry.GetAttributeValue(FIELD_NAME_DIRECTORY_VISIBILITY) + data.Description = user.Entry.GetAttributeValue("description") + data.ProfilePicture = user.Entry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE) if r.Method == "POST" { //5MB maximum size files @@ -56,7 +56,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) { } data.Visibility = visible - profilePicture, err := uploadProfilePicture(w, r, login) + profilePicture, err := uploadProfilePicture(w, r, user) if err != nil { data.ErrorMessage = err.Error() } @@ -65,7 +65,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) { data.ProfilePicture = profilePicture } - modify_request := ldap.NewModifyRequest(login.Info.DN, nil) + modify_request := ldap.NewModifyRequest(user.Login.Info.DN(), nil) modify_request.Replace("displayname", []string{data.DisplayName}) modify_request.Replace("givenname", []string{data.GivenName}) modify_request.Replace("sn", []string{data.Surname}) @@ -75,7 +75,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) { modify_request.Replace(FIELD_NAME_PROFILE_PICTURE, []string{data.ProfilePicture}) } - err = login.conn.Modify(modify_request) + err = user.Login.conn.Modify(modify_request) if err != nil { data.ErrorMessage = err.Error() } else { @@ -88,7 +88,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) { } type PasswdTplData struct { - Status *LoginStatus + User *LoggedUser ErrorMessage string TooShortError bool NoMatchError bool @@ -98,13 +98,13 @@ type PasswdTplData struct { func handlePasswd(w http.ResponseWriter, r *http.Request) { templatePasswd := getTemplate("passwd.html") - login := checkLogin(w, r) - if login == nil { + user := RequireUserHtml(w, r) + if user == nil { return } data := &PasswdTplData{ - Status: login, + User: user, ErrorMessage: "", Success: false, } @@ -120,11 +120,11 @@ func handlePasswd(w http.ResponseWriter, r *http.Request) { } else if password2 != password { data.NoMatchError = true } else { - modify_request := ldap.NewModifyRequest(login.Info.DN, nil) + modify_request := ldap.NewModifyRequest(user.Login.Info.DN(), nil) pw, err := SSHAEncode(password) if err == nil { modify_request.Replace("userpassword", []string{pw}) - err := login.conn.Modify(modify_request) + err := user.Login.conn.Modify(modify_request) if err != nil { data.ErrorMessage = err.Error() } else { diff --git a/quotas.go b/quotas.go new file mode 100644 index 0000000..e077ac8 --- /dev/null +++ b/quotas.go @@ -0,0 +1,79 @@ +package main + +import ( + "errors" + "fmt" + "strconv" + + "github.com/go-ldap/ldap/v3" + garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" +) + +const ( + // --- Default Quota Values --- + QUOTA_WEBSITE_SIZE_DEFAULT = 1024 * 1024 * 50 // 50MB + QUOTA_WEBSITE_SIZE_BURSTED = 1024 * 1024 * 200 // 200MB + QUOTA_WEBSITE_OBJECTS = 10000 // 10k objects + QUOTA_WEBSITE_COUNT = 5 // 5 buckets + + // --- Per-user overridable fields --- + FIELD_QUOTA_WEBSITE_SIZE_BURSTED = "quota_website_size_bursted" + FIELD_QUOTA_WEBSITE_COUNT = "quota_website_count" +) + +type UserQuota struct { + WebsiteCount int64 + WebsiteSizeDefault int64 + WebsiteSizeBursted int64 + WebsiteObjects int64 +} + +func NewUserQuota() *UserQuota { + return &UserQuota { + WebsiteCount: QUOTA_WEBSITE_COUNT, + WebsiteSizeDefault: QUOTA_WEBSITE_SIZE_DEFAULT, + WebsiteSizeBursted: QUOTA_WEBSITE_SIZE_BURSTED, + WebsiteObjects: QUOTA_WEBSITE_OBJECTS, + } +} + +var ( + ErrQuotaEmpty = fmt.Errorf("No quota is defined for this entry") + ErrQuotaInvalid = fmt.Errorf("The defined quota can't be parsed") +) + +func entryToQuota(entry *ldap.Entry, field string) (int64, error) { + f := entry.GetAttributeValue(field) + if f == "" { + return -1, ErrQuotaEmpty + } + + q, err := strconv.ParseInt(f, 10, 64) + if err != nil { + return -1, errors.Join(ErrQuotaInvalid, err) + } + return q, nil +} + +func NewUserQuotaFromEntry(entry *ldap.Entry) *UserQuota { + quotas := NewUserQuota() + + if q, err := entryToQuota(entry, FIELD_QUOTA_WEBSITE_COUNT); err != nil { + quotas.WebsiteCount = q + } + + if q, err := entryToQuota(entry, FIELD_QUOTA_WEBSITE_SIZE_BURSTED); err != nil { + quotas.WebsiteSizeBursted = q + } + + return quotas +} + +func (q *UserQuota) DefaultWebsiteQuota() *garage.UpdateBucketRequestQuotas { + qr := garage.NewUpdateBucketRequestQuotas() + + qr.SetMaxSize(q.WebsiteSizeDefault) + qr.SetMaxObjects(q.WebsiteSizeBursted) + + return qr +} From be97a1be587f42a4688825244b025b06172c442a Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 25 Sep 2023 15:57:59 +0200 Subject: [PATCH 09/14] fix some templates --- quotas.go | 4 ++-- templates/garage_key.html | 20 ++++++++++---------- templates/garage_website_list.html | 2 +- templates/home.html | 6 +++--- templates/profile.html | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/quotas.go b/quotas.go index e077ac8..3eec9b8 100644 --- a/quotas.go +++ b/quotas.go @@ -58,11 +58,11 @@ func entryToQuota(entry *ldap.Entry, field string) (int64, error) { func NewUserQuotaFromEntry(entry *ldap.Entry) *UserQuota { quotas := NewUserQuota() - if q, err := entryToQuota(entry, FIELD_QUOTA_WEBSITE_COUNT); err != nil { + 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 { + if q, err := entryToQuota(entry, FIELD_QUOTA_WEBSITE_SIZE_BURSTED); err == nil { quotas.WebsiteSizeBursted = q } diff --git a/templates/garage_key.html b/templates/garage_key.html index b839fcb..e1a9019 100644 --- a/templates/garage_key.html +++ b/templates/garage_key.html @@ -21,12 +21,12 @@ - - + + - + @@ -58,12 +58,12 @@ -
+

Créez un fichier nommé ~/.awsrc :

-export AWS_ACCESS_KEY_ID={{ .Key.AccessKeyId }}
-export AWS_SECRET_ACCESS_KEY={{ .Key.SecretAccessKey }}
+export AWS_ACCESS_KEY_ID={{ .S3KeyInfo.AccessKeyId }}
+export AWS_SECRET_ACCESS_KEY={{ .S3KeyInfo.SecretAccessKey }}
 export AWS_DEFAULT_REGION='garage'
 
 function aws { command aws --endpoint-url https://garage.deuxfleurs.fr $@ ; }
@@ -97,8 +97,8 @@ aws s3 cp /tmp/a.txt s3://my-bucket
 mc alias set \
   garage \
   https://garage.deuxfleurs.fr \
-  {{ .Key.AccessKeyId }} \
-  {{ .Key.SecretAccessKey }} \
+  {{ .S3KeyInfo.AccessKeyId }} \
+  {{ .S3KeyInfo.SecretAccessKey }} \
   --api S3v4
                         

Et ensuite pour utiliser Minio CLI avec :

@@ -176,7 +176,7 @@ hugo deploy
- + @@ -207,7 +207,7 @@ hugo deploy

Un exemple avec SCP :

-scp -oHostKeyAlgorithms=+ssh-rsa -P2222 -r ./public {{ .Status.Info.Username }}@bagage.deuxfleurs.fr:mon_bucket/
+scp -oHostKeyAlgorithms=+ssh-rsa -P2222 -r ./public {{ .Login.Info.Username }}@bagage.deuxfleurs.fr:mon_bucket/
                         
diff --git a/templates/garage_website_list.html b/templates/garage_website_list.html index ded8096..0f4a3b3 100644 --- a/templates/garage_website_list.html +++ b/templates/garage_website_list.html @@ -15,7 +15,7 @@ - {{ range $buck := .Key.Buckets }} + {{ range $buck := .S3KeyInfo.Buckets }} {{ if $buck.GlobalAliases }}
Identifiant de clé{{ .Key.AccessKeyId }}Identifiant de clé{{ .S3KeyInfo.AccessKeyId }}
Clé secrète{{ .Key.SecretAccessKey }}Cliquer pour afficher la clé secrète
Région
Nom d'utilisateur-ice{{ .Status.Info.Username }}{{ .Login.Info.Username }}
Mot de passe URLs
diff --git a/templates/home.html b/templates/home.html index 241a59d..3dad6b6 100644 --- a/templates/home.html +++ b/templates/home.html @@ -2,7 +2,7 @@ {{define "body"}}
- Bienvenue, {{ .Login.WelcomeName }} ! + Bienvenue, {{ .User.WelcomeName }} !
Se déconnecter @@ -33,7 +33,7 @@
-{{if .Login.CanInvite}} +{{if .User.Capabilities.CanInvite}}
Inviter des gens sur Deuxfleurs @@ -45,7 +45,7 @@
{{end}} -{{if .Login.CanAdmin}} +{{if .User.Capabilities.CanAdmin}}
Administration diff --git a/templates/profile.html b/templates/profile.html index 56461eb..17965a6 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -20,7 +20,7 @@
- +
From bc368943a4c0853718b8a53b1caadc297412ef32 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 25 Sep 2023 19:07:07 +0200 Subject: [PATCH 10/14] heavy refactor in progress --- api.go | 2 +- garage.go | 128 ++++++++-------- main.go | 10 +- templates/garage_key.html | 2 +- templates/garage_website_inspect.html | 93 ++++++------ templates/garage_website_list.html | 38 ----- templates/home.html | 4 +- website.go | 211 ++++++++++++++++++++++++++ 8 files changed, 333 insertions(+), 155 deletions(-) delete mode 100644 templates/garage_website_list.html create mode 100644 website.go diff --git a/api.go b/api.go index 7d9c2cd..fd6df93 100644 --- a/api.go +++ b/api.go @@ -29,7 +29,7 @@ type BucketRequest struct { http *http.Request } -func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) { +func handleAPIWebsite(w http.ResponseWriter, r *http.Request) { br, err := buildBucketRequest(w, r) if err != nil { diff --git a/garage.go b/garage.go index 4586e26..8c8633f 100644 --- a/garage.go +++ b/garage.go @@ -49,7 +49,7 @@ func grgGetKey(accessKey string) (*garage.KeyInfo, error) { -func grgCreateWebsite(gkey, bucket string, quotas *UserQuota) (*garage.BucketInfo, error) { +func grgCreateBucket(bucket string) (*garage.BucketInfo, error) { client, ctx := gadmin() br := garage.NewCreateBucketRequest() @@ -61,32 +61,40 @@ func grgCreateWebsite(gkey, bucket string, quotas *UserQuota) (*garage.BucketInf fmt.Printf("%+v\n", err) return nil, err } + return binfo, nil +} + +func grgAllowKeyOnBucket(bid, gkey string) (*garage.BucketInfo, error) { + client, ctx := gadmin() // Allow user's key ar := garage.AllowBucketKeyRequest{ - BucketId: *binfo.Id, + BucketId: bid, AccessKeyId: gkey, Permissions: *garage.NewAllowBucketKeyRequestPermissions(true, true, true), } - binfo, _, err = client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute() + binfo, _, err := client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute() if err != nil { fmt.Printf("%+v\n", err) return nil, err } - // Expose website and set quota + return binfo, nil +} + +func allowWebsiteDefault() *garage.UpdateBucketRequestWebsiteAccess { wr := garage.NewUpdateBucketRequestWebsiteAccess() wr.SetEnabled(true) wr.SetIndexDocument("index.html") wr.SetErrorDocument("error.html") - qr := quotas.DefaultWebsiteQuota() + return wr +} - ur := garage.NewUpdateBucketRequest() - ur.SetWebsiteAccess(*wr) - ur.SetQuotas(*qr) +func grgUpdateBucket(bid string, ur *garage.UpdateBucketRequest) (*garage.BucketInfo, error) { + client, ctx := gadmin() - binfo, _, err = client.BucketApi.UpdateBucket(ctx, *binfo.Id).UpdateBucketRequest(*ur).Execute() + binfo, _, err := client.BucketApi.UpdateBucket(ctx, bid).UpdateBucketRequest(*ur).Execute() if err != nil { fmt.Printf("%+v\n", err) return nil, err @@ -154,7 +162,7 @@ func grgGetBucket(bid string) (*garage.BucketInfo, error) { // --- Start page rendering functions -func handleGarageKey(w http.ResponseWriter, r *http.Request) { +func handleWebsiteConfigure(w http.ResponseWriter, r *http.Request) { user := RequireUserHtml(w, r) if user == nil { return @@ -164,22 +172,48 @@ func handleGarageKey(w http.ResponseWriter, r *http.Request) { tKey.Execute(w, user) } -func handleGarageWebsiteList(w http.ResponseWriter, r *http.Request) { +func handleWebsiteList(w http.ResponseWriter, r *http.Request) { user := RequireUserHtml(w, r) if user == nil { return } - tWebsiteList := getTemplate("garage_website_list.html") - tWebsiteList.Execute(w, user) + ctrl, err := NewWebsiteController(user) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + list := ctrl.List() + if len(list) > 0 { + http.Redirect(w, r, "/website/inspect/"+list[0].Pretty, http.StatusFound) + } else { + http.Redirect(w, r, "/website/new", http.StatusFound) + } } -func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) { +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: ctrl, + err: nil, + } + tWebsiteNew := getTemplate("garage_website_new.html") if r.Method == "POST" { r.ParseForm() @@ -188,73 +222,47 @@ func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) { if bucket == "" { bucket = strings.Join(r.Form["bucket2"], "") } - if bucket == "" { - log.Println("Form empty") - // @FIXME we need to return the error to the user - tWebsiteNew.Execute(w, nil) - return - } - keyInfo, err := user.S3KeyInfo() + view, err := ctrl.Create(bucket) if err != nil { - log.Println(err) - // @FIXME we need to return the error to the user - tWebsiteNew.Execute(w, nil) - return + tpl.err = err + tWebsiteNew.Execute(w, tpl) } - binfo, err := grgCreateWebsite(*keyInfo.AccessKeyId, bucket, user.Quota) - if err != nil { - log.Println(err) - // @FIXME we need to return the error to the user - tWebsiteNew.Execute(w, nil) - return - } - - http.Redirect(w, r, "/garage/website/b/"+*binfo.Id, http.StatusFound) + http.Redirect(w, r, "/website/inspect/"+view.Name.Pretty, http.StatusFound) return } tWebsiteNew.Execute(w, nil) } -type webInspectView struct { - User *LoggedUser - Bucket *garage.BucketInfo - IndexDoc string - ErrorDoc string - MaxObjects int64 - MaxSize int64 - UsedSizePct float64 +type WebsiteInspectTpl struct { + Ctrl *WebsiteController + View *WebsiteView } -func handleGarageWebsiteInspect(w http.ResponseWriter, r *http.Request) { +func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) { user := RequireUserHtml(w, r) if user == nil { return } - bucketId := mux.Vars(r)["bucket"] - // @FIXME check that user owns the bucket.... + ctrl, err := NewWebsiteController(user) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } - binfo, err := grgGetBucket(bucketId) + bucketName := mux.Vars(r)["bucket"] + + view, err := ctrl.Inspect(bucketName) if err != nil { - log.Println(err) - return + http.Error(w, err.Error(), http.StatusInternalServerError) + return } - wc := binfo.GetWebsiteConfig() - q := binfo.GetQuotas() - - view := webInspectView{ - User: user, - Bucket: binfo, - IndexDoc: (&wc).GetIndexDocument(), - ErrorDoc: (&wc).GetErrorDocument(), - MaxObjects: (&q).GetMaxObjects(), - MaxSize: (&q).GetMaxSize(), - } + tpl := &WebsiteInspectTpl{ ctrl, view } tWebsiteInspect := getTemplate("garage_website_inspect.html") - tWebsiteInspect.Execute(w, &view) + tWebsiteInspect.Execute(w, &tpl) } diff --git a/main.go b/main.go index ee1863c..34a1630 100644 --- a/main.go +++ b/main.go @@ -147,7 +147,7 @@ func server(args []string) { r.HandleFunc("/login", handleLogin) r.HandleFunc("/logout", handleLogout) - r.HandleFunc("/api/unstable/garage/bucket/{bucket}", handleAPIGarageBucket) + r.HandleFunc("/api/unstable/website/{bucket}", handleAPIWebsite) r.HandleFunc("/profile", handleProfile) r.HandleFunc("/passwd", handlePasswd) @@ -156,10 +156,10 @@ func server(args []string) { r.HandleFunc("/directory/search", handleDirectorySearch) r.HandleFunc("/directory", handleDirectory) - r.HandleFunc("/garage/key", handleGarageKey) - r.HandleFunc("/garage/website", handleGarageWebsiteList) - r.HandleFunc("/garage/website/new", handleGarageWebsiteNew) - r.HandleFunc("/garage/website/b/{bucket}", handleGarageWebsiteInspect) + r.HandleFunc("/website", handleWebsiteList) + r.HandleFunc("/website/new", handleWebsiteNew) + r.HandleFunc("/website/configure", handleWebsiteConfigure) + r.HandleFunc("/website/inspect/{bucket}", handleWebsiteInspect) r.HandleFunc("/invite/new_account", handleInviteNewAccount) r.HandleFunc("/invite/send_code", handleInviteSendCode) diff --git a/templates/garage_key.html b/templates/garage_key.html index e1a9019..cf56822 100644 --- a/templates/garage_key.html +++ b/templates/garage_key.html @@ -3,7 +3,7 @@ {{define "body"}} diff --git a/templates/garage_website_inspect.html b/templates/garage_website_inspect.html index bc60711..d5f48c2 100644 --- a/templates/garage_website_inspect.html +++ b/templates/garage_website_inspect.html @@ -2,57 +2,54 @@ {{define "body"}} - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID{{ .Bucket.Id }}
URLs - {{ range $alias := .Bucket.GlobalAliases }} - {{ if contains $alias "." }} - https://{{ $alias }} - {{ else }} - https://{{ $alias }}.web.deuxfleurs.fr - {{ end }} - {{ end }} -
Document d'index {{ .IndexDoc }}
Document d'erreur{{ .ErrorDoc }}
Nombre de fichiers{{ .Bucket.Objects }} / {{ .MaxObjects }}
Espace utilisé{{ .Bucket.Bytes }} / {{ .MaxSize }} octets
+
+
+
+ {{ $view := .View }} + {{ range $wid := .Ctrl.List }} + {{ if eq $wid.Internal $view.Name.Internal }} + + {{ $wid.Url }} + + {{ else }} + + {{ $wid.Url }} + + {{ end }} + {{ end }} +
+
+
+

{{ .View.Name.Url }}

-

Configurer le nom de domaine

+
Quotas
+
+
+ {{ .View.Size.Ratio }}% +
+
-{{ range $alias := .Bucket.GlobalAliases }} -{{ if contains $alias "." }} -

Le nom de domaine {{ $alias }} n'est pas géré par Deuxfleurs, il vous revient donc de configurer la zone DNS. Vous devez ajouter une entrée CNAME garage.deuxfleurs.fr ou ALIAS garage.deuxfleurs.fr auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).

-{{ else }} -

Le nom de domaine https://{{ $alias }}.web.deuxfleurs.fr est fourni par Deuxfleurs, il n'y a pas de configuration à faire.

-{{ end }} +

+ {{ .View.Size.PrettyCurrent }} utilisé sur un maximum de {{ .View.Size.PrettyMax }} + {{ if gt .View.Files.Ratio 0.5 }} +
{{ .View.Files.Current }} fichiers sur un maximum de {{ .View.Files.Max }} + {{ end }} +

+ + + {{ if .View.Name.Expanded }} +
Vous ne savez pas comment configurer votre nom de domaine ?
+

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 CNAME garage.deuxfleurs.fr ou ALIAS garage.deuxfleurs.fr auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).

+ {{ end }} + + +
+
{{ end }} -{{end}} diff --git a/templates/garage_website_list.html b/templates/garage_website_list.html deleted file mode 100644 index 0f4a3b3..0000000 --- a/templates/garage_website_list.html +++ /dev/null @@ -1,38 +0,0 @@ -{{define "title"}}Sites webs |{{end}} - -{{define "body"}} - - - - - - - - - - {{ range $buck := .S3KeyInfo.Buckets }} - {{ if $buck.GlobalAliases }} - - - - - {{ end }} - {{ end }} - -
IDURLs
- {{$buck.Id}} - - {{ range $alias := $buck.GlobalAliases }} - {{ if contains $alias "." }} - https://{{ $alias }} - {{ else }} - https://{{ $alias }}.web.deuxfleurs.fr - {{ end }} - {{ end }} -
-{{end}} diff --git a/templates/home.html b/templates/home.html index 3dad6b6..3475795 100644 --- a/templates/home.html +++ b/templates/home.html @@ -27,8 +27,8 @@ Garage
diff --git a/website.go b/website.go new file mode 100644 index 0000000..c06ccbc --- /dev/null +++ b/website.go @@ -0,0 +1,211 @@ +package main + +import ( + "fmt" + "sort" + "strings" + garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" +) + +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.)") +) + +type QuotaStat struct { + Current int64 + Max int64 + Ratio float64 + Burstable bool +} +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) +} + +type WebsiteId struct { + Pretty string + Internal string + Alt []string + Expanded bool + Url string + +} +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 +} + +func (w *WebsiteController) List() []*WebsiteId { + r := make([]*WebsiteId, 0, len(w.PrettyList)) + for _, k := range w.PrettyList { + r = append(r, w.WebsiteIdx[k]) + } + return r +} + +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(patch *WebsitePatch) (*WebsiteView, error) { + return nil, 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 +} + + +type WebsiteView struct { + Name *WebsiteId + Size QuotaStat + Files QuotaStat +} + +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 +} From 08287375736a0a57c03b4bd6d9c222dc5567db6e Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 25 Sep 2023 22:00:46 +0200 Subject: [PATCH 11/14] patch and delete with quota are now implemented --- garage.go | 45 ++++++++--- quotas.go | 71 ++++++++++++++++ templates/garage_website_inspect.html | 34 +++++++- templates/garage_website_new.html | 13 ++- website.go | 111 ++++++++++++++------------ 5 files changed, 210 insertions(+), 64 deletions(-) diff --git a/garage.go b/garage.go index 8c8633f..d3d4d38 100644 --- a/garage.go +++ b/garage.go @@ -160,6 +160,16 @@ func grgGetBucket(bid string) (*garage.BucketInfo, error) { } +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) { @@ -193,8 +203,8 @@ func handleWebsiteList(w http.ResponseWriter, r *http.Request) { } type WebsiteNewTpl struct { - ctrl *WebsiteController - err error + Ctrl *WebsiteController + Err error } func handleWebsiteNew(w http.ResponseWriter, r *http.Request) { @@ -209,10 +219,7 @@ func handleWebsiteNew(w http.ResponseWriter, r *http.Request) { return } - tpl := &WebsiteNewTpl{ - ctrl: ctrl, - err: nil, - } + tpl := &WebsiteNewTpl{ctrl, nil} tWebsiteNew := getTemplate("garage_website_new.html") if r.Method == "POST" { @@ -225,23 +232,27 @@ func handleWebsiteNew(w http.ResponseWriter, r *http.Request) { view, err := ctrl.Create(bucket) if err != nil { - tpl.err = err + tpl.Err = err tWebsiteNew.Execute(w, tpl) + return } http.Redirect(w, r, "/website/inspect/"+view.Name.Pretty, http.StatusFound) return } - tWebsiteNew.Execute(w, nil) + tWebsiteNew.Execute(w, tpl) } type WebsiteInspectTpl struct { Ctrl *WebsiteController View *WebsiteView + Err error } func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) { + var processErr error + user := RequireUserHtml(w, r) if user == nil { return @@ -254,6 +265,22 @@ func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) { } bucketName := mux.Vars(r)["bucket"] + + if r.Method == "POST" { + r.ParseForm() + action := strings.Join(r.Form["action"],"") + switch action { + case "increase_quota": + _, processErr = ctrl.Patch(bucketName, &WebsitePatch { size: &user.Quota.WebsiteSizeBursted }) + case "delete_bucket": + processErr = ctrl.Delete(bucketName) + http.Redirect(w, r, "/website", http.StatusFound) + return + default: + processErr = fmt.Errorf("Unknown action") + } + + } view, err := ctrl.Inspect(bucketName) if err != nil { @@ -261,7 +288,7 @@ func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) { return } - tpl := &WebsiteInspectTpl{ ctrl, view } + tpl := &WebsiteInspectTpl{ ctrl, view, processErr } tWebsiteInspect := getTemplate("garage_website_inspect.html") tWebsiteInspect.Execute(w, &tpl) diff --git a/quotas.go b/quotas.go index 3eec9b8..9a2e426 100644 --- a/quotas.go +++ b/quotas.go @@ -77,3 +77,74 @@ func (q *UserQuota) DefaultWebsiteQuota() *garage.UpdateBucketRequestQuotas { 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 + Max int64 + Ratio float64 + Burstable bool +} +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) +} diff --git a/templates/garage_website_inspect.html b/templates/garage_website_inspect.html index d5f48c2..c062ab9 100644 --- a/templates/garage_website_inspect.html +++ b/templates/garage_website_inspect.html @@ -2,15 +2,27 @@ {{define "body"}} -
-
-
+
+ {{ if .Err }} +
+
{{ .Err.Error }}
+
+ {{ end }} + +
+ + + + + Nouveau site web + + +
{{ $view := .View }} {{ range $wid := .Ctrl.List }} {{ if eq $wid.Internal $view.Name.Internal }} @@ -24,6 +36,11 @@ {{ end }} {{ end }}
+ +

+ {{ .Ctrl.WebsiteCount.Current }} sites créés sur {{ .Ctrl.WebsiteCount.Max }}
+ Jusqu'à {{ .Ctrl.User.Quota.WebsiteSizeBurstedPretty }} par site web +

{{ .View.Name.Url }}

@@ -42,6 +59,15 @@ {{ end }}

+
Actions
+
+
+ + Changer le nom de domaine + +
+
+ {{ if .View.Name.Expanded }}
Vous ne savez pas comment configurer votre nom de domaine ?
diff --git a/templates/garage_website_new.html b/templates/garage_website_new.html index f1cd847..7ee4936 100644 --- a/templates/garage_website_new.html +++ b/templates/garage_website_new.html @@ -3,8 +3,16 @@ {{define "body"}} + +
+
+ {{if .Err}} +
{{ .Err.Error }}
+ {{end}} +
+
diff --git a/website.go b/website.go index c06ccbc..6e86b19 100644 --- a/website.go +++ b/website.go @@ -15,55 +15,11 @@ var ( ErrCantCreateBucket = fmt.Errorf("Can't create this bucket. Maybe another bucket already exists with this name or you have an invalid character") ErrCantAllowKey = fmt.Errorf("Can't allow given key on the target bucket") ErrCantConfigureBucket = fmt.Errorf("Unable to configure the bucket (activating website, adding quotas, etc.)") + ErrBucketDeleteNotEmpty = fmt.Errorf("You must remove all the files before deleting a bucket") + ErrBucketDeleteUnfinishedUpload = fmt.Errorf("You must remove all the unfinished multipart uploads before deleting a bucket") ) -type QuotaStat struct { - Current int64 - Max int64 - Ratio float64 - Burstable bool -} -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) -} type WebsiteId struct { Pretty string @@ -146,8 +102,36 @@ func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) { return NewWebsiteView(binfo), nil } -func (w *WebsiteController) Patch(patch *WebsitePatch) (*WebsiteView, error) { - return nil, nil +func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteView, error) { + website, ok := w.WebsiteIdx[pretty] + if !ok { + return nil, ErrWebsiteNotFound + } + + binfo, err := grgGetBucket(website.Internal) + if err != nil { + return nil, ErrFetchBucketInfo + } + + // Patch the max size + urQuota := garage.NewUpdateBucketRequestQuotas() + urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(binfo.Quotas.GetMaxSize())) + urQuota.SetMaxObjects(w.User.Quota.WebsiteObjectAdjust(binfo.Quotas.GetMaxObjects())) + if patch.size != nil { + urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(*patch.size)) + } + + // Build the update + ur := garage.NewUpdateBucketRequest() + ur.SetQuotas(*urQuota) + + // Call garage + binfo, err = grgUpdateBucket(website.Internal, ur) + if err != nil { + return nil, ErrCantConfigureBucket + } + + return NewWebsiteView(binfo), nil } func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) { @@ -190,6 +174,35 @@ func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) { 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 @@ -207,5 +220,5 @@ func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView { } type WebsitePatch struct { - size int64 + size *int64 } From 982bd8a43c50bb5845b694dbd0b3e0ffbf43dad7 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 25 Sep 2023 23:00:57 +0200 Subject: [PATCH 12/14] done with API --- api.go | 228 +++++++++++++++++--------------------------- garage.go | 8 +- main.go | 3 +- quotas.go | 8 +- templates/home.html | 4 +- website.go | 33 ++++--- 6 files changed, 116 insertions(+), 168 deletions(-) diff --git a/api.go b/api.go index fd6df93..c804276 100644 --- a/api.go +++ b/api.go @@ -1,48 +1,29 @@ package main import ( - //"context" "errors" "encoding/json" - garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" + "fmt" "github.com/gorilla/mux" - "log" "net/http" ) -type ApiQuotaView struct { - files *uint64 - size *uint64 -} +func handleAPIWebsiteList(w http.ResponseWriter, r *http.Request) { + user := RequireUserApi(w, r) -type ApiBucketView struct { - global *bool - max *ApiQuotaView - used *ApiQuotaView -} - -type BucketRequest struct { - s3key *garage.KeyInfo - bucketName string - bucketId string - global bool - http *http.Request -} - -func handleAPIWebsite(w http.ResponseWriter, r *http.Request) { - - br, err := buildBucketRequest(w, r) - if err != nil { - return + if user == nil { + return } - if r.Method == http.MethodPatch { - patchGarageBucket(w, br) + ctrl, err := NewWebsiteController(user) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) return } if r.Method == http.MethodGet { - getGarageBucket(w, br) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(ctrl.Describe()) return } @@ -50,131 +31,92 @@ func handleAPIWebsite(w http.ResponseWriter, r *http.Request) { return } -func buildBucketRequest(w http.ResponseWriter, r *http.Request) (*BucketRequest, error) { +func handleAPIWebsiteInspect(w http.ResponseWriter, r *http.Request) { user := RequireUserApi(w, r) + if user == nil { - return nil, errors.New("Unable to fetch user") + return } - // FETCH BUCKET ID by iterating over buckets owned by this key bucketName := mux.Vars(r)["bucket"] - var bucketId *string - var global *bool - - s3key, err := user.S3KeyInfo() + ctrl, err := NewWebsiteController(user) if err != nil { - return nil, err - } - -findBucketIdLoop: - for _, bucket := range s3key.Buckets { - for _, localAlias := range bucket.LocalAliases { - if localAlias == bucketName { - bucketId = bucket.Id - *global = false - break findBucketIdLoop - } - } - for _, globalAlias := range bucket.GlobalAliases { - if globalAlias == bucketName { - bucketId = bucket.Id - *global = true - break findBucketIdLoop - } - } - } - - if bucketId == nil || global == nil { - http.Error(w, "Bucket not found in this account", http.StatusNotFound) - return nil, errors.New("Unable to fetch bucket ID") - } - - return &BucketRequest{ - s3key: s3key, - bucketName: bucketName, - bucketId: *bucketId, - global: *global, - http: r, - }, nil - -} - -func patchGarageBucket(w http.ResponseWriter, br *BucketRequest) { - var err error - - // DECODE BODY - var queuedChange ApiBucketView - decoder := json.NewDecoder(br.http.Body) - err = decoder.Decode(&queuedChange) - if err != nil { - log.Println(err) - http.Error(w, "Unable to decode the body", http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusInternalServerError) return } - // SET THE GLOBAL FLAG - if queuedChange.global != nil { - if *queuedChange.global && !br.global { - _, err = grgAddGlobalAlias(br.bucketId, br.bucketName) - if err != nil { - http.Error(w, "Unable to add the requested name as global alias for this bucket", http.StatusInternalServerError) - return - } - _, err = grgDelLocalAlias(br.bucketId, *br.s3key.AccessKeyId, br.bucketName) - if err != nil { - http.Error(w, "Unable to remove the local alias for this bucket", http.StatusInternalServerError) - return - } - } else if !*queuedChange.global && br.global { - grgAddLocalAlias(br.bucketId, *br.s3key.AccessKeyId, br.bucketName) - if err != nil { - http.Error(w, "Unable to add the requested name as local alias for this bucket", http.StatusInternalServerError) - return - } - grgDelGlobalAlias(br.bucketId, br.bucketName) - if err != nil { - http.Error(w, "Unable to remove the global alias for this bucket", 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 } - } - - // CHECK IF QUOTA MUST BE ADDED TO THIS BUCKET - - // VALIDATE IT - // --- global --- - // 1. can be true, false, or nil (use pointers) - // 2. if nil do nothing - // 3. if false, throw "not yet implemented" (501) - // 4. if true, check that the bucket name does not exist yet in the global namespace, throw "forbidden" (403) - // --- quota.size --- - // 1. if no quota on the bucket + this field is none, set to 50MB - // 2. if lower than 50MB, set to 50MB. If higher than 200MB, set to 200MB - // --- quota.files --- - // 1. if no quota on the bucket + this field is none, set to 10k - // 2. if lower than 10k, set to 10k. If higher than 40k, set to 40k - // READ BODY JSON - - // IF BODY.GLOBAL is not NONE - // DO: Add an alias - - // IF BODY.QUOTA.SIZE is not NONE - // DO: Change quota - - // IF BODY.QUOTA.FILE is not NONE - // DO: Change quota - - getGarageBucket(w, br) -} - -func getGarageBucket(w http.ResponseWriter, br *BucketRequest) { - // FETCH AN UPDATED BUCKET VIEW - bucket, err := grgGetBucket(br.bucketId) - if err != nil { - http.Error(w, "Unable to fetch bucket details", http.StatusInternalServerError) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(view) return } - // BUILD A VIEW - log.Println(bucket) + 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 } diff --git a/garage.go b/garage.go index d3d4d38..b3ca836 100644 --- a/garage.go +++ b/garage.go @@ -194,9 +194,9 @@ func handleWebsiteList(w http.ResponseWriter, r *http.Request) { return } - list := ctrl.List() - if len(list) > 0 { - http.Redirect(w, r, "/website/inspect/"+list[0].Pretty, http.StatusFound) + desc := ctrl.Describe() + if len(desc.Websites) > 0 { + http.Redirect(w, r, "/website/inspect/"+desc.Websites[0].Pretty, http.StatusFound) } else { http.Redirect(w, r, "/website/new", http.StatusFound) } @@ -271,7 +271,7 @@ func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) { action := strings.Join(r.Form["action"],"") switch action { case "increase_quota": - _, processErr = ctrl.Patch(bucketName, &WebsitePatch { size: &user.Quota.WebsiteSizeBursted }) + _, processErr = ctrl.Patch(bucketName, &WebsitePatch { Size: &user.Quota.WebsiteSizeBursted }) case "delete_bucket": processErr = ctrl.Delete(bucketName) http.Redirect(w, r, "/website", http.StatusFound) diff --git a/main.go b/main.go index 34a1630..9763f53 100644 --- a/main.go +++ b/main.go @@ -147,7 +147,8 @@ func server(args []string) { r.HandleFunc("/login", handleLogin) r.HandleFunc("/logout", handleLogout) - r.HandleFunc("/api/unstable/website/{bucket}", handleAPIWebsite) + r.HandleFunc("/api/unstable/website", handleAPIWebsiteList) + r.HandleFunc("/api/unstable/website/{bucket}", handleAPIWebsiteInspect) r.HandleFunc("/profile", handleProfile) r.HandleFunc("/passwd", handlePasswd) diff --git a/quotas.go b/quotas.go index 9a2e426..e520f5c 100644 --- a/quotas.go +++ b/quotas.go @@ -102,10 +102,10 @@ func (q *UserQuota) WebsiteSizeBurstedPretty() string { // --- A quota stat we can use type QuotaStat struct { - Current int64 - Max int64 - Ratio float64 - Burstable bool + 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 { diff --git a/templates/home.html b/templates/home.html index 3475795..dd88d13 100644 --- a/templates/home.html +++ b/templates/home.html @@ -24,11 +24,11 @@
- Garage + Mon espace sur la toile
diff --git a/website.go b/website.go index 6e86b19..7e89a41 100644 --- a/website.go +++ b/website.go @@ -22,11 +22,11 @@ var ( type WebsiteId struct { - Pretty string - Internal string - Alt []string - Expanded bool - Url string + 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 { @@ -50,7 +50,7 @@ func NewWebsiteIdFromBucketInfo(binfo *garage.BucketInfo) *WebsiteId { } type WebsiteController struct { - User *LoggedUser + User *LoggedUser WebsiteIdx map[string]*WebsiteId PrettyList []string WebsiteCount QuotaStat @@ -80,12 +80,17 @@ func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) { return &WebsiteController { user, idx, wlist, quota }, nil } -func (w *WebsiteController) List() []*WebsiteId { +type WebsiteDescribe struct { + AllowedWebsites *QuotaStat `json:"quota"` + Websites []*WebsiteId `json:"vhosts"` +} + +func (w *WebsiteController) Describe() *WebsiteDescribe { r := make([]*WebsiteId, 0, len(w.PrettyList)) for _, k := range w.PrettyList { r = append(r, w.WebsiteIdx[k]) } - return r + return &WebsiteDescribe { &w.WebsiteCount, r } } func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) { @@ -117,8 +122,8 @@ func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteV 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)) + if patch.Size != nil { + urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(*patch.Size)) } // Build the update @@ -205,9 +210,9 @@ func (w *WebsiteController) Delete(pretty string) error { type WebsiteView struct { - Name *WebsiteId - Size QuotaStat - Files QuotaStat + Name *WebsiteId `json:"identified_as"` + Size QuotaStat `json:"quota_size"` + Files QuotaStat `json:"quota_files"` } func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView { @@ -220,5 +225,5 @@ func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView { } type WebsitePatch struct { - size *int64 + Size *int64 `json:"quota_size"` } From d8633d7fb89b55996f25e1a67180408c805ba15f Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 26 Sep 2023 08:40:30 +0200 Subject: [PATCH 13/14] final --- api.go | 7 ++++++- garage.go | 15 ++++++++++----- integration/config/bottin.json | 10 ++++++++-- invite.go | 1 + templates/garage_website_inspect.html | 6 +++--- website.go | 19 ++++++++++++++++--- 6 files changed, 44 insertions(+), 14 deletions(-) diff --git a/api.go b/api.go index c804276..73fd322 100644 --- a/api.go +++ b/api.go @@ -22,8 +22,13 @@ func handleAPIWebsiteList(w http.ResponseWriter, r *http.Request) { } 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(ctrl.Describe()) + json.NewEncoder(w).Encode(describe) return } diff --git a/garage.go b/garage.go index b3ca836..236dcbd 100644 --- a/garage.go +++ b/garage.go @@ -194,9 +194,8 @@ func handleWebsiteList(w http.ResponseWriter, r *http.Request) { return } - desc := ctrl.Describe() - if len(desc.Websites) > 0 { - http.Redirect(w, r, "/website/inspect/"+desc.Websites[0].Pretty, http.StatusFound) + 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) } @@ -245,7 +244,7 @@ func handleWebsiteNew(w http.ResponseWriter, r *http.Request) { } type WebsiteInspectTpl struct { - Ctrl *WebsiteController + Describe *WebsiteDescribe View *WebsiteView Err error } @@ -287,8 +286,14 @@ func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) { 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{ ctrl, view, processErr } + tpl := &WebsiteInspectTpl{ describe, view, processErr } tWebsiteInspect := getTemplate("garage_website_inspect.html") tWebsiteInspect.Execute(w, &tpl) diff --git a/integration/config/bottin.json b/integration/config/bottin.json index 0b54e22..4b9f3d7 100644 --- a/integration/config/bottin.json +++ b/integration/config/bottin.json @@ -6,8 +6,14 @@ "ANONYMOUS::bind:*,ou=users,dc=bottin,dc=eu:", "ANONYMOUS::bind:cn=admin,dc=bottin,dc=eu:", "*,dc=bottin,dc=eu::read:*:* !userpassword", - "*::read modify:SELF:*", "cn=admin,dc=bottin,dc=eu::read add modify delete:*:*", - "*:cn=admin,ou=groups,dc=bottin,dc=eu:read add modify delete:*:*" + "*:cn=admin,ou=groups,dc=bottin,dc=eu:read add modify delete:*:*", + + "ANONYMOUS::bind:*,ou=invitations,dc=bottin,dc=eu:", + "*,ou=invitations,dc=bottin,dc=eu::delete:SELF:*", + "*,ou=invitations,dc=bottin,dc=eu::add:*,ou=users,dc=bottin,dc=eu:*", + "*,ou=invitations,dc=bottin,dc=eu::modifyAdd:cn=email,ou=groups,dc=bottin,dc=eu:*", + + "*::read modify:SELF:*" ] } diff --git a/invite.go b/invite.go index 0a0e836..060947a 100644 --- a/invite.go +++ b/invite.go @@ -60,6 +60,7 @@ func handleInvitationCode(w http.ResponseWriter, r *http.Request) { inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN err = l.Bind(inviteDn, code_pw) if err != nil { + log.Println(err) templateInviteInvalidCode := getTemplate("invite_invalid_code.html") templateInviteInvalidCode.Execute(w, nil) return diff --git a/templates/garage_website_inspect.html b/templates/garage_website_inspect.html index c062ab9..37142df 100644 --- a/templates/garage_website_inspect.html +++ b/templates/garage_website_inspect.html @@ -24,7 +24,7 @@
{{ $view := .View }} - {{ range $wid := .Ctrl.List }} + {{ range $wid := .Describe.Websites }} {{ if eq $wid.Internal $view.Name.Internal }} {{ $wid.Url }} @@ -38,8 +38,8 @@

- {{ .Ctrl.WebsiteCount.Current }} sites créés sur {{ .Ctrl.WebsiteCount.Max }}
- Jusqu'à {{ .Ctrl.User.Quota.WebsiteSizeBurstedPretty }} par site web + {{ .Describe.AllowedWebsites.Current }} sites créés sur {{ .Describe.AllowedWebsites.Max }}
+ Jusqu'à {{ .Describe.BurstBucketQuotaSize }} par site web

diff --git a/website.go b/website.go index 7e89a41..6158042 100644 --- a/website.go +++ b/website.go @@ -81,16 +81,29 @@ func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) { } type WebsiteDescribe struct { - AllowedWebsites *QuotaStat `json:"quota"` + 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 { +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 { &w.WebsiteCount, r } + return &WebsiteDescribe { + *s3key.AccessKeyId, + *s3key.SecretAccessKey, + &w.WebsiteCount, + w.User.Quota.WebsiteSizeBurstedPretty(), + r }, nil } func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) { From 706ff58a6f6608719feda15075d50f978df39c5b Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 26 Sep 2023 08:40:41 +0200 Subject: [PATCH 14/14] format --- api.go | 12 ++++---- cli.go | 20 +++++++------- garage.go | 57 ++++++++++++++++++-------------------- login.go | 45 +++++++++++++++--------------- main.go | 23 ++++++++-------- profile.go | 4 +-- quotas.go | 45 +++++++++++++++--------------- website.go | 80 +++++++++++++++++++++++++----------------------------- 8 files changed, 139 insertions(+), 147 deletions(-) diff --git a/api.go b/api.go index 73fd322..e99fce5 100644 --- a/api.go +++ b/api.go @@ -1,8 +1,8 @@ package main import ( - "errors" "encoding/json" + "errors" "fmt" "github.com/gorilla/mux" "net/http" @@ -12,7 +12,7 @@ func handleAPIWebsiteList(w http.ResponseWriter, r *http.Request) { user := RequireUserApi(w, r) if user == nil { - return + return } ctrl, err := NewWebsiteController(user) @@ -40,7 +40,7 @@ func handleAPIWebsiteInspect(w http.ResponseWriter, r *http.Request) { user := RequireUserApi(w, r) if user == nil { - return + return } bucketName := mux.Vars(r)["bucket"] @@ -86,7 +86,7 @@ func handleAPIWebsiteInspect(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPatch { var patch WebsitePatch err := json.NewDecoder(r.Body).Decode(&patch) - if err != nil { + 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 } @@ -98,7 +98,7 @@ func handleAPIWebsiteInspect(w http.ResponseWriter, r *http.Request) { } else if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return - } + } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(view) @@ -119,7 +119,7 @@ func handleAPIWebsiteInspect(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusNoContent) return - + } http.Error(w, "This method is not implemented for this endpoint", http.StatusNotImplemented) diff --git a/cli.go b/cli.go index df11460..2d45a4c 100644 --- a/cli.go +++ b/cli.go @@ -3,9 +3,9 @@ package main import ( "flag" "fmt" - "os" - "syscall" "golang.org/x/term" + "os" + "syscall" ) var fsCli = flag.NewFlagSet("cli", flag.ContinueOnError) @@ -27,18 +27,18 @@ func cliMain(args []string) { func cliPasswd() { fmt.Print("Password: ") - bytepw, err := term.ReadPassword(int(syscall.Stdin)) - if err != nil { + bytepw, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { fmt.Println(err) - os.Exit(1) - } - pass := string(bytepw) + os.Exit(1) + } + pass := string(bytepw) hash, err := SSHAEncode(pass) - if err != nil { + if err != nil { fmt.Println(err) - os.Exit(1) - } + os.Exit(1) + } fmt.Println(hash) } diff --git a/garage.go b/garage.go index 236dcbd..c43fd5f 100644 --- a/garage.go +++ b/garage.go @@ -10,7 +10,6 @@ import ( "strings" ) - func gadmin() (*garage.APIClient, context.Context) { // Set Host and other parameters configuration := garage.NewConfiguration() @@ -47,8 +46,6 @@ func grgGetKey(accessKey string) (*garage.KeyInfo, error) { return resp, nil } - - func grgCreateBucket(bucket string) (*garage.BucketInfo, error) { client, ctx := gadmin() @@ -64,7 +61,7 @@ func grgCreateBucket(bucket string) (*garage.BucketInfo, error) { return binfo, nil } -func grgAllowKeyOnBucket(bid, gkey string) (*garage.BucketInfo, error) { +func grgAllowKeyOnBucket(bid, gkey string) (*garage.BucketInfo, error) { client, ctx := gadmin() // Allow user's key @@ -163,10 +160,10 @@ func grgGetBucket(bid string) (*garage.BucketInfo, error) { func grgDeleteBucket(bid string) error { client, ctx := gadmin() - _, err := client.BucketApi.DeleteBucket(ctx, bid).Execute() - if err != nil { + _, err := client.BucketApi.DeleteBucket(ctx, bid).Execute() + if err != nil { log.Println(err) - } + } return err } @@ -189,9 +186,9 @@ func handleWebsiteList(w http.ResponseWriter, r *http.Request) { } ctrl, err := NewWebsiteController(user) - if err != nil { + if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return + return } if len(ctrl.PrettyList) > 0 { @@ -203,7 +200,7 @@ func handleWebsiteList(w http.ResponseWriter, r *http.Request) { type WebsiteNewTpl struct { Ctrl *WebsiteController - Err error + Err error } func handleWebsiteNew(w http.ResponseWriter, r *http.Request) { @@ -213,9 +210,9 @@ func handleWebsiteNew(w http.ResponseWriter, r *http.Request) { } ctrl, err := NewWebsiteController(user) - if err != nil { + if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return + return } tpl := &WebsiteNewTpl{ctrl, nil} @@ -245,8 +242,8 @@ func handleWebsiteNew(w http.ResponseWriter, r *http.Request) { type WebsiteInspectTpl struct { Describe *WebsiteDescribe - View *WebsiteView - Err error + View *WebsiteView + Err error } func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) { @@ -258,42 +255,42 @@ func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) { } ctrl, err := NewWebsiteController(user) - if err != nil { + if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return + return } bucketName := mux.Vars(r)["bucket"] if r.Method == "POST" { r.ParseForm() - action := strings.Join(r.Form["action"],"") + action := strings.Join(r.Form["action"], "") switch action { - case "increase_quota": - _, processErr = ctrl.Patch(bucketName, &WebsitePatch { Size: &user.Quota.WebsiteSizeBursted }) - case "delete_bucket": - processErr = ctrl.Delete(bucketName) - http.Redirect(w, r, "/website", http.StatusFound) - return - default: - processErr = fmt.Errorf("Unknown action") + case "increase_quota": + _, processErr = ctrl.Patch(bucketName, &WebsitePatch{Size: &user.Quota.WebsiteSizeBursted}) + case "delete_bucket": + processErr = ctrl.Delete(bucketName) + http.Redirect(w, r, "/website", http.StatusFound) + return + default: + processErr = fmt.Errorf("Unknown action") } } - + view, err := ctrl.Inspect(bucketName) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return + return } - + describe, err := ctrl.Describe() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return + return } - tpl := &WebsiteInspectTpl{ describe, view, processErr } + tpl := &WebsiteInspectTpl{describe, view, processErr} tWebsiteInspect := getTemplate("garage_website_inspect.html") tWebsiteInspect.Execute(w, &tpl) diff --git a/login.go b/login.go index 87b7a67..277e3ae 100644 --- a/login.go +++ b/login.go @@ -13,12 +13,12 @@ import ( 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") + 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") + ErrLDAPSearchNotFound = fmt.Errorf("User is authenticated but its associated data can not be found during search") ) // --- Login Info --- @@ -48,10 +48,10 @@ func NewLoginInfoFromSession(r *http.Request) (*LoginInfo, error) { func NewLoginInfoFromBasicAuth(r *http.Request) (*LoginInfo, error) { username, password, ok := r.BasicAuth() if ok { - login_info := &LoginInfo{ - Username: username, - Password: password, - } + login_info := &LoginInfo{ + Username: username, + Password: password, + } return login_info, nil } @@ -69,8 +69,8 @@ func (li *LoginInfo) DN() string { // --- Login Status --- type LoginStatus struct { - Info *LoginInfo - conn *ldap.Conn + Info *LoginInfo + conn *ldap.Conn } func NewLoginStatus(r *http.Request, login_info *LoginInfo) (*LoginStatus, error) { @@ -109,12 +109,13 @@ func NewLdapCon() (*ldap.Conn, error) { // --- Capabilities --- type Capabilities struct { - CanAdmin bool + CanAdmin bool CanInvite bool } + func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities { // Initialize - canAdmin := false + canAdmin := false canInvite := false // Special case for the "admin" account that is de-facto admin @@ -135,19 +136,20 @@ func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities { } return &Capabilities{ - CanAdmin: canAdmin, + CanAdmin: canAdmin, CanInvite: canInvite, } } // --- Logged User --- type LoggedUser struct { - Login *LoginStatus + Login *LoginStatus Entry *ldap.Entry Capabilities *Capabilities - Quota *UserQuota - s3key *garage.KeyInfo + Quota *UserQuota + s3key *garage.KeyInfo } + func NewLoggedUser(login *LoginStatus) (*LoggedUser, error) { requestKind := "(objectClass=organizationalPerson)" if strings.EqualFold(login.Info.DN(), config.AdminAccount) { @@ -184,11 +186,11 @@ func NewLoggedUser(login *LoginStatus) (*LoggedUser, error) { } entry := sr.Entries[0] - lu := &LoggedUser { - Login: login, - Entry: entry, + lu := &LoggedUser{ + Login: login, + Entry: entry, Capabilities: NewCapabilities(login, entry), - Quota: NewUserQuotaFromEntry(entry), + Quota: NewUserQuotaFromEntry(entry), } return lu, nil } @@ -251,7 +253,6 @@ func RequireUser(r *http.Request) (*LoggedUser, error) { return nil, ErrNotAuthenticated } - loginStatus, err := NewLoginStatus(r, login_info) if err != nil { return nil, err diff --git a/main.go b/main.go index 9763f53..6553bef 100644 --- a/main.go +++ b/main.go @@ -119,13 +119,13 @@ func main() { } 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) + case "cli": + cliMain(os.Args[2:]) + case "server": + server(os.Args[2:]) + default: + log.Println("Usage: guichet [server|cli] --help") + os.Exit(1) } } @@ -192,10 +192,9 @@ func logRequest(handler http.Handler) http.Handler { // Page handlers ---- - // --- Home Controller type HomePageData struct { - User *LoggedUser + User *LoggedUser BaseDN string } @@ -208,7 +207,7 @@ func handleHome(w http.ResponseWriter, r *http.Request) { } data := &HomePageData{ - User: user, + User: user, BaseDN: config.BaseDN, } @@ -235,7 +234,7 @@ func handleLogout(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/login", http.StatusFound) } -// --- Login Controller --- +// --- Login Controller --- type LoginFormData struct { Username string WrongUser bool @@ -254,7 +253,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) { username := strings.Join(r.Form["username"], "") password := strings.Join(r.Form["password"], "") - loginInfo := LoginInfo { username, password } + loginInfo := LoginInfo{username, password} l, err := NewLdapCon() if err != nil { diff --git a/profile.go b/profile.go index 58e7d96..bd7e299 100644 --- a/profile.go +++ b/profile.go @@ -8,7 +8,7 @@ import ( ) type ProfileTplData struct { - User *LoggedUser + User *LoggedUser ErrorMessage string Success bool Mail string @@ -29,7 +29,7 @@ func handleProfile(w http.ResponseWriter, r *http.Request) { } data := &ProfileTplData{ - User: user, + User: user, ErrorMessage: "", Success: false, } diff --git a/quotas.go b/quotas.go index e520f5c..894ea3c 100644 --- a/quotas.go +++ b/quotas.go @@ -5,40 +5,40 @@ import ( "fmt" "strconv" - "github.com/go-ldap/ldap/v3" 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_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 + 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" + FIELD_QUOTA_WEBSITE_COUNT = "quota_website_count" ) type UserQuota struct { - WebsiteCount int64 + WebsiteCount int64 WebsiteSizeDefault int64 WebsiteSizeBursted int64 - WebsiteObjects int64 + WebsiteObjects int64 } func NewUserQuota() *UserQuota { - return &UserQuota { - WebsiteCount: QUOTA_WEBSITE_COUNT, + return &UserQuota{ + WebsiteCount: QUOTA_WEBSITE_COUNT, WebsiteSizeDefault: QUOTA_WEBSITE_SIZE_DEFAULT, WebsiteSizeBursted: QUOTA_WEBSITE_SIZE_BURSTED, - WebsiteObjects: QUOTA_WEBSITE_OBJECTS, + WebsiteObjects: QUOTA_WEBSITE_OBJECTS, } } var ( - ErrQuotaEmpty = fmt.Errorf("No quota is defined for this entry") + ErrQuotaEmpty = fmt.Errorf("No quota is defined for this entry") ErrQuotaInvalid = fmt.Errorf("The defined quota can't be parsed") ) @@ -72,7 +72,7 @@ func NewUserQuotaFromEntry(entry *ldap.Entry) *UserQuota { func (q *UserQuota) DefaultWebsiteQuota() *garage.UpdateBucketRequestQuotas { qr := garage.NewUpdateBucketRequestQuotas() - qr.SetMaxSize(q.WebsiteSizeDefault) + qr.SetMaxSize(q.WebsiteSizeDefault) qr.SetMaxObjects(q.WebsiteSizeBursted) return qr @@ -80,7 +80,7 @@ func (q *UserQuota) DefaultWebsiteQuota() *garage.UpdateBucketRequestQuotas { func (q *UserQuota) WebsiteSizeAdjust(sz int64) int64 { if sz < q.WebsiteSizeDefault { - return q.WebsiteSizeDefault + return q.WebsiteSizeDefault } else if sz > q.WebsiteSizeBursted { return q.WebsiteSizeBursted } else { @@ -102,16 +102,17 @@ func (q *UserQuota) WebsiteSizeBurstedPretty() string { // --- 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"` + 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), + return QuotaStat{ + Current: current, + Max: max, + Ratio: float64(current) / float64(max), Burstable: burstable, } } @@ -141,7 +142,7 @@ func prettyValue(v int64) string { if v < 1024 { return fmt.Sprintf("%d Mio", v) } - v = v / 1024 + v = v / 1024 if v < 1024 { return fmt.Sprintf("%d Gio", v) } diff --git a/website.go b/website.go index 6158042..ba432c5 100644 --- a/website.go +++ b/website.go @@ -2,33 +2,31 @@ package main import ( "fmt" + garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" "sort" "strings" - garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" ) 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") + ErrWebsiteNotFound = fmt.Errorf("Website not found") + ErrFetchBucketInfo = fmt.Errorf("Failed to fetch bucket information") + ErrWebsiteQuotaReached = fmt.Errorf("Can't create additional websites, quota reached") + ErrEmptyBucketName = fmt.Errorf("You can't create a website with an empty name") + ErrCantCreateBucket = fmt.Errorf("Can't create this bucket. Maybe another bucket already exists with this name or you have an invalid character") + ErrCantAllowKey = fmt.Errorf("Can't allow given key on the target bucket") + ErrCantConfigureBucket = fmt.Errorf("Unable to configure the bucket (activating website, adding quotas, etc.)") + ErrBucketDeleteNotEmpty = fmt.Errorf("You must remove all the files before deleting a bucket") ErrBucketDeleteUnfinishedUpload = fmt.Errorf("You must remove all the unfinished multipart uploads before deleting a bucket") ) - - type WebsiteId struct { - Pretty string `json:"name"` - Internal string `json:"-"` - Alt []string `json:"alt_name"` - Expanded bool `json:"expanded"` - Url string `json:"domain"` - + 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 @@ -43,16 +41,16 @@ func NewWebsiteId(id string, aliases []string) *WebsiteId { url = fmt.Sprintf("%s.web.deuxfleurs.fr", pretty) } - return &WebsiteId { pretty, id, alt, expanded, url } + 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 + User *LoggedUser + WebsiteIdx map[string]*WebsiteId + PrettyList []string WebsiteCount QuotaStat } @@ -65,7 +63,7 @@ func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) { return nil, err } - for _, bckt := range(keyInfo.Buckets) { + for _, bckt := range keyInfo.Buckets { if len(bckt.GlobalAliases) > 0 { wid := NewWebsiteId(*bckt.Id, bckt.GlobalAliases) idx[wid.Pretty] = wid @@ -77,15 +75,15 @@ func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) { maxW := user.Quota.WebsiteCount quota := NewQuotaStat(int64(len(wlist)), maxW, true) - return &WebsiteController { user, idx, wlist, quota }, nil + 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"` + 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) { @@ -96,14 +94,14 @@ func (w *WebsiteController) Describe() (*WebsiteDescribe, error) { r := make([]*WebsiteId, 0, len(w.PrettyList)) for _, k := range w.PrettyList { - r = append(r, w.WebsiteIdx[k]) + r = append(r, w.WebsiteIdx[k]) } - return &WebsiteDescribe { - *s3key.AccessKeyId, - *s3key.SecretAccessKey, - &w.WebsiteCount, + return &WebsiteDescribe{ + *s3key.AccessKeyId, + *s3key.SecretAccessKey, + &w.WebsiteCount, w.User.Quota.WebsiteSizeBurstedPretty(), - r }, nil + r}, nil } func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) { @@ -183,7 +181,6 @@ func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) { ur.SetWebsiteAccess(*wr) ur.SetQuotas(*qr) - binfo, err = grgUpdateBucket(*binfo.Id, ur) if err != nil { return nil, ErrCantConfigureBucket @@ -209,7 +206,7 @@ func (w *WebsiteController) Delete(pretty string) error { if *binfo.Objects > int64(0) { return ErrBucketDeleteNotEmpty - } + } if *binfo.UnfinishedUploads > int32(0) { return ErrBucketDeleteUnfinishedUpload @@ -219,13 +216,10 @@ func (w *WebsiteController) Delete(pretty string) error { return err } - - - type WebsiteView struct { - Name *WebsiteId `json:"identified_as"` - Size QuotaStat `json:"quota_size"` - Files QuotaStat `json:"quota_files"` + Name *WebsiteId `json:"identified_as"` + Size QuotaStat `json:"quota_size"` + Files QuotaStat `json:"quota_files"` } func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView { @@ -234,7 +228,7 @@ func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView { wid := NewWebsiteIdFromBucketInfo(binfo) size := NewQuotaStat(*binfo.Bytes, (&q).GetMaxSize(), true) objects := NewQuotaStat(*binfo.Objects, (&q).GetMaxObjects(), false) - return &WebsiteView { wid, size, objects } + return &WebsiteView{wid, size, objects} } type WebsitePatch struct {