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