api

Paddy 2015-12-14 Parent:57c9412e8000

2:9b954c219259 Go to Latest

api/api.go

Update scope helpers and add access denied error. Refactor CheckScopes to take the Scopes as input instead of reading them from the passed request. Also, surface a GetScopes method that will pull the Scopes from the request and return them. While we're at it, we refactored to use the sort package instead of a manual search. We also needed an access_denied error, and that will come up a lot, so it was worth making a constant, just like our invalid_format error.

History
paddy@0 1 package api
paddy@0 2
paddy@0 3 import (
paddy@0 4 "encoding/json"
paddy@0 5 "errors"
paddy@0 6 "log"
paddy@0 7 "net/http"
paddy@2 8 "sort"
paddy@0 9 "strings"
paddy@0 10
paddy@0 11 "bitbucket.org/ww/goautoneg"
paddy@0 12
paddy@0 13 "code.secondbit.org/uuid.hg"
paddy@0 14
paddy@0 15 "golang.org/x/net/context"
paddy@0 16 )
paddy@0 17
paddy@0 18 const (
paddy@0 19 RequestErrAccessDenied = "access_denied"
paddy@0 20 RequestErrInsufficient = "insufficient"
paddy@0 21 RequestErrOverflow = "overflow"
paddy@0 22 RequestErrInvalidValue = "invalid_value"
paddy@0 23 RequestErrInvalidFormat = "invalid_format"
paddy@0 24 RequestErrMissing = "missing"
paddy@0 25 RequestErrNotFound = "not_found"
paddy@0 26 RequestErrConflict = "conflict"
paddy@0 27 RequestErrActOfGod = "act_of_god"
paddy@0 28 )
paddy@0 29
paddy@0 30 var (
paddy@0 31 ActOfGodError = []RequestError{{Slug: RequestErrActOfGod}}
paddy@0 32 InvalidFormatError = []RequestError{{Slug: RequestErrInvalidFormat, Field: "/"}}
paddy@2 33 AccessDeniedError = []RequestError{{Slug: RequestErrAccessDenied}}
paddy@0 34
paddy@0 35 Encoders = []string{"application/json"}
paddy@0 36
paddy@0 37 ErrUserIDNotSet = errors.New("user ID not set")
paddy@0 38 )
paddy@0 39
paddy@0 40 type RequestError struct {
paddy@0 41 Slug string `json:"error,omitempty"`
paddy@0 42 Field string `json:"field,omitempty"`
paddy@0 43 Param string `json:"param,omitempty"`
paddy@0 44 Header string `json:"header,omitempty"`
paddy@0 45 }
paddy@0 46
paddy@1 47 type ContextHandler func(context.Context, http.ResponseWriter, *http.Request)
paddy@0 48
paddy@0 49 func NegotiateMiddleware(h http.Handler) http.Handler {
paddy@0 50 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
paddy@0 51 if r.Header.Get("Accept") != "" {
paddy@0 52 contentType := goautoneg.Negotiate(r.Header.Get("Accept"), Encoders)
paddy@0 53 if contentType == "" {
paddy@0 54 w.WriteHeader(http.StatusNotAcceptable)
paddy@0 55 w.Write([]byte("Unsupported content type requested: " + r.Header.Get("Accept")))
paddy@0 56 return
paddy@0 57 }
paddy@0 58 }
paddy@0 59 h.ServeHTTP(w, r)
paddy@0 60 })
paddy@0 61 }
paddy@0 62
paddy@0 63 func Encode(w http.ResponseWriter, r *http.Request, status int, resp interface{}) {
paddy@0 64 contentType := goautoneg.Negotiate(r.Header.Get("Accept"), Encoders)
paddy@0 65 w.Header().Set("content-type", contentType)
paddy@0 66 w.WriteHeader(status)
paddy@0 67 var err error
paddy@0 68 switch contentType {
paddy@0 69 case "application/json":
paddy@0 70 enc := json.NewEncoder(w)
paddy@0 71 err = enc.Encode(resp)
paddy@0 72 default:
paddy@0 73 enc := json.NewEncoder(w)
paddy@0 74 err = enc.Encode(resp)
paddy@0 75 }
paddy@0 76 if err != nil {
paddy@0 77 log.Println(err)
paddy@0 78 }
paddy@0 79 }
paddy@0 80
paddy@0 81 func Decode(r *http.Request, target interface{}) error {
paddy@0 82 defer r.Body.Close()
paddy@0 83 switch r.Header.Get("Content-Type") {
paddy@0 84 case "application/json":
paddy@0 85 dec := json.NewDecoder(r.Body)
paddy@0 86 return dec.Decode(target)
paddy@0 87 default:
paddy@0 88 dec := json.NewDecoder(r.Body)
paddy@0 89 return dec.Decode(target)
paddy@0 90 }
paddy@0 91 }
paddy@0 92
paddy@0 93 func CORSMiddleware(h http.Handler) http.Handler {
paddy@0 94 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
paddy@0 95 w.Header().Set("Access-Control-Allow-Origin", "*")
paddy@0 96 w.Header().Set("Access-Control-Allow-Headers", r.Header.Get("Access-Control-Request-Headers"))
paddy@0 97 w.Header().Set("Access-Control-Allow-Credentials", "true")
paddy@0 98 if strings.ToLower(r.Method) == "options" {
paddy@0 99 methods := strings.Join(r.Header[http.CanonicalHeaderKey("Trout-Methods")], ", ")
paddy@0 100 w.Header().Set("Access-Control-Allow-Methods", methods)
paddy@0 101 w.Header().Set("Allow", methods)
paddy@0 102 w.WriteHeader(http.StatusOK)
paddy@0 103 return
paddy@0 104 }
paddy@0 105 h.ServeHTTP(w, r)
paddy@0 106 })
paddy@0 107 }
paddy@0 108
paddy@0 109 func ContextWrapper(c context.Context, handler ContextHandler) http.Handler {
paddy@0 110 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
paddy@1 111 handler(c, w, r)
paddy@0 112 })
paddy@0 113 }
paddy@0 114
paddy@2 115 func CheckScopes(scopes []string, checking ...string) bool {
paddy@2 116 sort.Strings(scopes)
paddy@2 117 for _, scope := range checking {
paddy@2 118 found := sort.SearchStrings(scopes, scope)
paddy@2 119 if found == len(scopes) || scopes[found] != scope {
paddy@0 120 return false
paddy@0 121 }
paddy@0 122 }
paddy@0 123 return true
paddy@0 124 }
paddy@0 125
paddy@2 126 func GetScopes(r *http.Request) []string {
paddy@2 127 scopes := strings.Split(r.Header.Get("scopes"), " ")
paddy@2 128 for pos, scope := range scopes {
paddy@2 129 scopes[pos] = strings.TrimSpace(scope)
paddy@2 130 }
paddy@2 131 sort.Strings(scopes)
paddy@2 132 return scopes
paddy@2 133 }
paddy@2 134
paddy@0 135 func AuthUser(r *http.Request) (uuid.ID, error) {
paddy@0 136 rawID := r.Header.Get("User-ID")
paddy@0 137 if rawID == "" {
paddy@0 138 return nil, ErrUserIDNotSet
paddy@0 139 }
paddy@0 140 return uuid.Parse(rawID)
paddy@0 141 }