// RecordDelete implements gaia's response to a DELETE request to the '/record/' endpoint. // // Assumptions: The user has been authenticated. // // Proceedings: Parses the url parameters, and retrieves the kind and id parameters (both required). // Then checks for authorization to delete, carries it out if allowed. // // Success: // * StatusNoContent indicating a succesful deletion // // Errors: // * InternalServerError: failure to parse the parameters, database connections, json marshalling // * BadRequest: no kind param, unrecognized kind, no id param, invalid id param // * NotFound: unauthorized, record actually doesn't exist // * Unauthorized: not authorized to delete that record, database access denial func RecordDELETE(ctx context.Context, w http.ResponseWriter, r *http.Request, logger services.Logger, db services.DB) { l := logger.WithPrefix("RecordDELETE: ") // Parse the form if err := r.ParseForm(); err != nil { l.Printf("error parsing form: %s", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } // Retrieve the kind parameter k := r.FormValue(kindParam) if k == "" { l.Printf("no kind specified") http.Error(w, fmt.Sprintf("You must specify a %q parameter", kindParam), http.StatusBadRequest) return } kind := data.Kind(k) // Retrieve the id parameter i := r.FormValue(idParam) if i == "" { l.Printf("no id specified") http.Error(w, fmt.Sprintf("You must specify a %q parameter", idParam), http.StatusBadRequest) return } // Verify the kind is recognized _, ok := models.Kinds[kind] if !ok { l.Printf("unrecognized kind: %q", kind) http.Error(w, fmt.Sprintf("The kind %q is not recognized", kind), http.StatusBadRequest) return } // Verify the id is valid id, err := db.ParseID(i) if err != nil { l.Printf("invalid id: %q, error: %s", i, err) http.Error(w, fmt.Sprintf("The id %q is invalid", i), http.StatusBadRequest) return } // Get the record, so that we can decide whether we have permission to delete it m := models.ModelFor(kind) m.SetID(id) if err = db.PopulateByID(m); err != nil { l.Printf("db.PopulateByID error: %s", err) switch err { case data.ErrAccessDenial: fallthrough // don't leak information (were we denied access, this record doesn't exist) case data.ErrNotFound: http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) case data.ErrNoConnection: fallthrough case data.ErrInvalidID: fallthrough default: http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } return } // Retrieve the user we are authenticated as u, ok := user.FromContext(ctx) if !ok { l.Print("faild to retrieve user from context") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } // check for authorization if allowed, err := access.CanDelete(db, u, m); err != nil { // TODO(nclandolfi) standardize this with the POST and GET where we handle the possible errors l.Printf("RecordDELETE Error: %s", err) http.Error(w, "database error", http.StatusInternalServerError) return } else if !allowed { // in order to not leak information, we treat this as a not found http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } if err := db.Delete(m); err != nil { switch err { case data.ErrAccessDenial: http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) // don't leak information case data.ErrNotFound: // this shouldn't happen unless it was deleted by another process // in between when we populated the record by id, in which case it was successful goto successfulDelete // all of these errors, we can't guarantee deletion case data.ErrNoConnection: fallthrough case data.ErrInvalidID: http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) default: l.Printf("RecordDELETE Error: %s", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } return } successfulDelete: w.WriteHeader(http.StatusNoContent) }
// RecordGET implements gaia's response to a GET request to the '/record/' endpoint. // // Assumptions: The user has been authenticated. // // Proceedings: Parses the url parameters, retrieving the kind and id parameters (both required). // Then it loads that record, checks if the user is allowed to access it, if so it returns the model as JSON. // // Success: // * StatusOK with the record as JSON // // Errors: // * InternalServerError: failure to parse the parameters, database connections, json marshalling // * BadRequest: no kind param, no id param, unrecognized kind, invalid id // * NotFound: unauthorized, record actually doesn't exist func RecordGET(ctx context.Context, w http.ResponseWriter, r *http.Request, logger services.Logger, db services.DB) { l := logger.WithPrefix("RecordGet: ") // Parse the form value if err := r.ParseForm(); err != nil { l.Printf("error parsing form: %s", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } // Secure the kind parameter's existence, and superficial validity (i.e., non-empty) k := r.FormValue(kindParam) if k == "" { l.Printf("no kind parameter") http.Error(w, fmt.Sprintf("You must specify a '%s' parameter", kindParam), http.StatusBadRequest) return } kind := data.Kind(k) // Secure the id parameter's existence, and superficial validity (i.e., non-empty) i := r.FormValue(idParam) if i == "" { l.Printf("no id parameter") http.Error(w, fmt.Sprintf("You must specify a '%s' parameter", idParam), http.StatusBadRequest) return } // Ensure the kind is recognized if _, ok := models.Kinds[kind]; !ok { l.Printf("unrecognized kind: %q", kind) http.Error(w, fmt.Sprintf("The kind %q is not recognized", kind), http.StatusBadRequest) return } // Ensure the id is valid id, err := db.ParseID(i) if err != nil { l.Printf("unrecognized id: %q, err: %s", i, err) http.Error(w, fmt.Sprintf("The id %q is invalid", i), http.StatusBadRequest) return } m := models.ModelFor(kind) m.SetID(id) if err := db.PopulateByID(m); err != nil { switch err { // ErrAccessDenial and ErrNotFound are "normal" courses, in the sense that they // may be expected in normal usage. case data.ErrAccessDenial: fallthrough // don't leak information, make it look like a 404 case data.ErrNotFound: // This is, by far, the most common error case here. http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) // ErrNoConnection, ErrInvalidID and the under-determined errors are all non-normal cases case data.ErrNoConnection: fallthrough case data.ErrInvalidID: fallthrough default: http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } l.Printf("db.PopulateByID error: %s", err) return // regardless of what the error was, we are bailing } // Retrieve the user this request was authenticated as u, ok := user.FromContext(ctx) if !ok { // This is certainly an issue, and should _never_ happen l.Print("failed to retrieve user from context") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } // Now we impose the system access control, beyond the database access control // TODO: limit the domain of errors CanRead returns if allowed, err := access.CanRead(db, u, m); err != nil { switch err { // Again, though odd, both of these are arguably expected case data.ErrAccessDenial: fallthrough // don't leak information case data.ErrNotFound: http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) // These are not-expected case data.ErrNoConnection: fallthrough case data.ErrInvalidID: fallthrough default: http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } l.Printf("access.CanRead error: %s", err) return } else if !allowed { // If you can't read the record you are asking for, // it "does not exist" as far as you are concerned l.Print("access denied") http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } bytes, err := json.MarshalIndent(m, "", " ") if err != nil { l.Printf("error while marshalling json %s", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") w.Write(bytes) }