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" "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(l) http.Error(w, "Internal server error", http.StatusInternalServerError) 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 } 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) { br, err := buildBucketRequest(w, r) if err != nil { return } if r.Method == http.MethodPatch { patchGarageBucket(w, br) return } if r.Method == http.MethodGet { getGarageBucket(w, br) return } 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 // 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) return } // BUILD A VIEW log.Println(bucket) }