auth

Paddy 2015-03-07 Parent:874c21d1dd8d Child:8fad4d66c7ea

139:026adb0c7fc4 Go to Latest

auth/client.go

Test our GetClientHandler function, add isAuthError helper. Add a helper that identifies whether the error passed to it is an authentication error or is some other type of error. This is useful fo checking whether or not an internal error occurred while authenticating users. Update all instances where we call our authentication helper to make them use the new error helper. All tests continue to pass. Add a new test case for retrieving a client as an unauthenticated user. This clears the client's secret from the response before sending it. Update the GetClientHandler function to return the secret when the owner of the client used Basic Auth in the request. Add a new test case for retrieving a client as an authenticated user, both the owner and a non-owner user. This makes sure the secret is divulged only in the appropriate cases.

History
paddy@6 1 package auth
paddy@0 2
paddy@0 3 import (
paddy@108 4 "crypto/rand"
paddy@108 5 "encoding/hex"
paddy@85 6 "encoding/json"
paddy@31 7 "errors"
paddy@116 8 "log"
paddy@85 9 "net/http"
paddy@41 10 "net/url"
paddy@115 11 "strconv"
paddy@135 12 "strings"
paddy@41 13 "time"
paddy@31 14
paddy@116 15 "github.com/PuerkitoBio/purell"
paddy@116 16 "github.com/gorilla/mux"
paddy@116 17
paddy@107 18 "code.secondbit.org/uuid.hg"
paddy@0 19 )
paddy@0 20
paddy@121 21 func init() {
paddy@121 22 RegisterGrantType("client_credentials", GrantType{
paddy@121 23 Validate: clientCredentialsValidate,
paddy@121 24 Invalidate: nil,
paddy@121 25 IssuesRefresh: true,
paddy@121 26 ReturnToken: RenderJSONToken,
paddy@123 27 AllowsPublic: false,
paddy@124 28 AuditString: clientCredentialsAuditString,
paddy@121 29 })
paddy@121 30 }
paddy@121 31
paddy@31 32 var (
paddy@57 33 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first.
paddy@57 34 ErrNoClientStore = errors.New("no clientStore was specified for the Context")
paddy@57 35 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore.
paddy@57 36 ErrClientNotFound = errors.New("client not found in clientStore")
paddy@57 37 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with
paddy@57 38 // the same ID already exists in the clientStore.
paddy@57 39 ErrClientAlreadyExists = errors.New("client already exists in clientStore")
paddy@41 40
paddy@57 41 // ErrEmptyChange is returned when a Change has all its properties set to nil.
paddy@57 42 ErrEmptyChange = errors.New("change must have at least one property set")
paddy@57 43 // ErrClientNameTooShort is returned when a Client's Name property is too short.
paddy@57 44 ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
paddy@57 45 // ErrClientNameTooLong is returned when a Client's Name property is too long.
paddy@57 46 ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
paddy@57 47 // ErrClientLogoTooLong is returned when a Client's Logo property is too long.
paddy@57 48 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
paddy@57 49 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL.
paddy@57 50 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
paddy@57 51 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long.
paddy@49 52 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
paddy@57 53 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL.
paddy@57 54 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
paddy@116 55 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL.
paddy@116 56 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL")
paddy@31 57 )
paddy@31 58
paddy@115 59 const (
paddy@138 60 clientTypePublic = "public"
paddy@138 61 clientTypeConfidential = "confidential"
paddy@138 62 minClientNameLen = 2
paddy@138 63 maxClientNameLen = 24
paddy@138 64 defaultClientResponseSize = 20
paddy@138 65 maxClientResponseSize = 50
paddy@138 66 defaultEndpointResponseSize = 20
paddy@138 67 maxEndpointResponseSize = 50
paddy@130 68
paddy@130 69 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery
paddy@115 70 )
paddy@115 71
paddy@25 72 // Client represents a client that grants access
paddy@25 73 // to the auth server, exchanging grants for tokens,
paddy@25 74 // and tokens for access.
paddy@0 75 type Client struct {
paddy@116 76 ID uuid.ID `json:"id,omitempty"`
paddy@116 77 Secret string `json:"secret,omitempty"`
paddy@116 78 OwnerID uuid.ID `json:"owner_id,omitempty"`
paddy@116 79 Name string `json:"name,omitempty"`
paddy@116 80 Logo string `json:"logo,omitempty"`
paddy@116 81 Website string `json:"website,omitempty"`
paddy@116 82 Type string `json:"type,omitempty"`
paddy@0 83 }
paddy@0 84
paddy@57 85 // ApplyChange applies the properties of the passed
paddy@57 86 // ClientChange to the Client object it is called on.
paddy@39 87 func (c *Client) ApplyChange(change ClientChange) {
paddy@39 88 if change.Secret != nil {
paddy@39 89 c.Secret = *change.Secret
paddy@39 90 }
paddy@39 91 if change.OwnerID != nil {
paddy@39 92 c.OwnerID = change.OwnerID
paddy@39 93 }
paddy@39 94 if change.Name != nil {
paddy@39 95 c.Name = *change.Name
paddy@39 96 }
paddy@39 97 if change.Logo != nil {
paddy@39 98 c.Logo = *change.Logo
paddy@39 99 }
paddy@39 100 if change.Website != nil {
paddy@39 101 c.Website = *change.Website
paddy@39 102 }
paddy@39 103 }
paddy@39 104
paddy@57 105 // ClientChange represents a bundle of options for
paddy@57 106 // updating a Client's mutable data.
paddy@31 107 type ClientChange struct {
paddy@41 108 Secret *string
paddy@41 109 OwnerID uuid.ID
paddy@41 110 Name *string
paddy@41 111 Logo *string
paddy@41 112 Website *string
paddy@31 113 }
paddy@31 114
paddy@57 115 // Validate checks the ClientChange it is called on
paddy@57 116 // and asserts its internal validity, or lack thereof.
paddy@133 117 func (c ClientChange) Validate() []error {
paddy@133 118 errors := []error{}
paddy@42 119 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
paddy@133 120 errors = append(errors, ErrEmptyChange)
paddy@133 121 return errors
paddy@42 122 }
paddy@41 123 if c.Name != nil && len(*c.Name) < 2 {
paddy@133 124 errors = append(errors, ErrClientNameTooShort)
paddy@41 125 }
paddy@41 126 if c.Name != nil && len(*c.Name) > 32 {
paddy@133 127 errors = append(errors, ErrClientNameTooLong)
paddy@41 128 }
paddy@42 129 if c.Logo != nil && *c.Logo != "" {
paddy@42 130 if len(*c.Logo) > 1024 {
paddy@133 131 errors = append(errors, ErrClientLogoTooLong)
paddy@42 132 }
paddy@42 133 u, err := url.Parse(*c.Logo)
paddy@42 134 if err != nil || !u.IsAbs() {
paddy@133 135 errors = append(errors, ErrClientLogoNotURL)
paddy@42 136 }
paddy@41 137 }
paddy@42 138 if c.Website != nil && *c.Website != "" {
paddy@42 139 if len(*c.Website) > 140 {
paddy@133 140 errors = append(errors, ErrClientWebsiteTooLong)
paddy@42 141 }
paddy@42 142 u, err := url.Parse(*c.Website)
paddy@42 143 if err != nil || !u.IsAbs() {
paddy@133 144 errors = append(errors, ErrClientWebsiteNotURL)
paddy@42 145 }
paddy@41 146 }
paddy@133 147 return errors
paddy@39 148 }
paddy@39 149
paddy@123 150 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) {
paddy@85 151 enc := json.NewEncoder(w)
paddy@85 152 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
paddy@85 153 if !fromAuthHeader {
paddy@85 154 clientIDStr = r.PostFormValue("client_id")
paddy@85 155 }
paddy@123 156 if clientIDStr == "" {
paddy@85 157 w.WriteHeader(http.StatusUnauthorized)
paddy@85 158 if fromAuthHeader {
paddy@85 159 w.Header().Set("WWW-Authenticate", "Basic")
paddy@85 160 }
paddy@85 161 renderJSONError(enc, "invalid_client")
paddy@123 162 return nil, "", false
paddy@123 163 }
paddy@129 164 if !allowPublic && !fromAuthHeader {
paddy@129 165 w.WriteHeader(http.StatusBadRequest)
paddy@129 166 renderJSONError(enc, "unauthorized_client")
paddy@129 167 return nil, "", false
paddy@129 168 }
paddy@123 169 clientID, err := uuid.Parse(clientIDStr)
paddy@123 170 if err != nil {
paddy@123 171 log.Println("Error decoding client ID:", err)
paddy@123 172 w.WriteHeader(http.StatusUnauthorized)
paddy@123 173 if fromAuthHeader {
paddy@123 174 w.Header().Set("WWW-Authenticate", "Basic")
paddy@123 175 }
paddy@123 176 renderJSONError(enc, "invalid_client")
paddy@123 177 return nil, "", false
paddy@123 178 }
paddy@123 179 return clientID, clientSecret, true
paddy@123 180 }
paddy@123 181
paddy@123 182 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
paddy@123 183 enc := json.NewEncoder(w)
paddy@123 184 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic)
paddy@123 185 if !ok {
paddy@85 186 return nil, false
paddy@85 187 }
paddy@123 188 _, _, fromAuthHeader := r.BasicAuth()
paddy@85 189 client, err := context.GetClient(clientID)
paddy@85 190 if err == ErrClientNotFound {
paddy@85 191 w.WriteHeader(http.StatusUnauthorized)
paddy@85 192 if fromAuthHeader {
paddy@85 193 w.Header().Set("WWW-Authenticate", "Basic")
paddy@85 194 }
paddy@85 195 renderJSONError(enc, "invalid_client")
paddy@85 196 return nil, false
paddy@85 197 } else if err != nil {
paddy@85 198 w.WriteHeader(http.StatusInternalServerError)
paddy@85 199 renderJSONError(enc, "server_error")
paddy@85 200 return nil, false
paddy@85 201 }
paddy@113 202 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
paddy@85 203 w.WriteHeader(http.StatusUnauthorized)
paddy@85 204 if fromAuthHeader {
paddy@85 205 w.Header().Set("WWW-Authenticate", "Basic")
paddy@85 206 }
paddy@85 207 renderJSONError(enc, "invalid_client")
paddy@85 208 return nil, false
paddy@85 209 }
paddy@85 210 return clientID, true
paddy@85 211 }
paddy@85 212
paddy@57 213 // Endpoint represents a single URI that a Client
paddy@57 214 // controls. Users will be redirected to these URIs
paddy@57 215 // following successful authorization grants and
paddy@57 216 // exchanges for access tokens.
paddy@41 217 type Endpoint struct {
paddy@116 218 ID uuid.ID `json:"id,omitempty"`
paddy@116 219 ClientID uuid.ID `json:"client_id,omitempty"`
paddy@116 220 URI string `json:"uri,omitempty"`
paddy@116 221 NormalizedURI string `json:"-"`
paddy@116 222 Added time.Time `json:"added,omitempty"`
paddy@116 223 }
paddy@116 224
paddy@116 225 func normalizeURIString(in string) (string, error) {
paddy@130 226 n, err := purell.NormalizeURLString(in, normalizeFlags)
paddy@116 227 if err != nil {
paddy@116 228 log.Println(err)
paddy@116 229 return in, ErrEndpointURINotURL
paddy@116 230 }
paddy@116 231 return n, nil
paddy@116 232 }
paddy@116 233
paddy@116 234 func normalizeURI(in *url.URL) string {
paddy@130 235 return purell.NormalizeURL(in, normalizeFlags)
paddy@41 236 }
paddy@41 237
paddy@41 238 type sortedEndpoints []Endpoint
paddy@41 239
paddy@41 240 func (s sortedEndpoints) Len() int {
paddy@41 241 return len(s)
paddy@41 242 }
paddy@41 243
paddy@41 244 func (s sortedEndpoints) Less(i, j int) bool {
paddy@41 245 return s[i].Added.Before(s[j].Added)
paddy@41 246 }
paddy@41 247
paddy@41 248 func (s sortedEndpoints) Swap(i, j int) {
paddy@41 249 s[i], s[j] = s[j], s[i]
paddy@41 250 }
paddy@41 251
paddy@57 252 type clientStore interface {
paddy@57 253 getClient(id uuid.ID) (Client, error)
paddy@57 254 saveClient(client Client) error
paddy@57 255 updateClient(id uuid.ID, change ClientChange) error
paddy@57 256 deleteClient(id uuid.ID) error
paddy@57 257 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
paddy@41 258
paddy@115 259 addEndpoints(client uuid.ID, endpoint []Endpoint) error
paddy@57 260 removeEndpoint(client, endpoint uuid.ID) error
paddy@58 261 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
paddy@57 262 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
paddy@57 263 countEndpoints(client uuid.ID) (int64, error)
paddy@0 264 }
paddy@31 265
paddy@57 266 func (m *memstore) getClient(id uuid.ID) (Client, error) {
paddy@31 267 m.clientLock.RLock()
paddy@31 268 defer m.clientLock.RUnlock()
paddy@31 269 c, ok := m.clients[id.String()]
paddy@31 270 if !ok {
paddy@31 271 return Client{}, ErrClientNotFound
paddy@31 272 }
paddy@31 273 return c, nil
paddy@31 274 }
paddy@31 275
paddy@57 276 func (m *memstore) saveClient(client Client) error {
paddy@31 277 m.clientLock.Lock()
paddy@31 278 defer m.clientLock.Unlock()
paddy@31 279 if _, ok := m.clients[client.ID.String()]; ok {
paddy@31 280 return ErrClientAlreadyExists
paddy@31 281 }
paddy@31 282 m.clients[client.ID.String()] = client
paddy@31 283 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
paddy@31 284 return nil
paddy@31 285 }
paddy@31 286
paddy@57 287 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
paddy@39 288 m.clientLock.Lock()
paddy@39 289 defer m.clientLock.Unlock()
paddy@39 290 c, ok := m.clients[id.String()]
paddy@39 291 if !ok {
paddy@39 292 return ErrClientNotFound
paddy@39 293 }
paddy@39 294 c.ApplyChange(change)
paddy@39 295 m.clients[id.String()] = c
paddy@31 296 return nil
paddy@31 297 }
paddy@31 298
paddy@57 299 func (m *memstore) deleteClient(id uuid.ID) error {
paddy@57 300 client, err := m.getClient(id)
paddy@31 301 if err != nil {
paddy@31 302 return err
paddy@31 303 }
paddy@31 304 m.clientLock.Lock()
paddy@31 305 defer m.clientLock.Unlock()
paddy@31 306 delete(m.clients, id.String())
paddy@31 307 pos := -1
paddy@31 308 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
paddy@31 309 if item.Equal(id) {
paddy@31 310 pos = p
paddy@31 311 break
paddy@31 312 }
paddy@31 313 }
paddy@31 314 if pos >= 0 {
paddy@31 315 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
paddy@31 316 }
paddy@31 317 return nil
paddy@31 318 }
paddy@31 319
paddy@57 320 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
paddy@33 321 ids := m.lookupClientsByProfileID(ownerID.String())
paddy@31 322 if len(ids) > num+offset {
paddy@31 323 ids = ids[offset : num+offset]
paddy@31 324 } else if len(ids) > offset {
paddy@31 325 ids = ids[offset:]
paddy@31 326 } else {
paddy@31 327 return []Client{}, nil
paddy@31 328 }
paddy@31 329 clients := []Client{}
paddy@31 330 for _, id := range ids {
paddy@57 331 client, err := m.getClient(id)
paddy@31 332 if err != nil {
paddy@31 333 return []Client{}, err
paddy@31 334 }
paddy@31 335 clients = append(clients, client)
paddy@31 336 }
paddy@31 337 return clients, nil
paddy@31 338 }
paddy@41 339
paddy@115 340 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error {
paddy@41 341 m.endpointLock.Lock()
paddy@41 342 defer m.endpointLock.Unlock()
paddy@115 343 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...)
paddy@41 344 return nil
paddy@41 345 }
paddy@41 346
paddy@57 347 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
paddy@41 348 m.endpointLock.Lock()
paddy@41 349 defer m.endpointLock.Unlock()
paddy@41 350 pos := -1
paddy@41 351 for p, item := range m.endpoints[client.String()] {
paddy@41 352 if item.ID.Equal(endpoint) {
paddy@41 353 pos = p
paddy@41 354 break
paddy@41 355 }
paddy@41 356 }
paddy@41 357 if pos >= 0 {
paddy@41 358 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
paddy@41 359 }
paddy@41 360 return nil
paddy@41 361 }
paddy@41 362
paddy@58 363 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
paddy@41 364 m.endpointLock.RLock()
paddy@41 365 defer m.endpointLock.RUnlock()
paddy@41 366 for _, candidate := range m.endpoints[client.String()] {
paddy@116 367 if endpoint == candidate.NormalizedURI {
paddy@41 368 return true, nil
paddy@41 369 }
paddy@41 370 }
paddy@41 371 return false, nil
paddy@41 372 }
paddy@41 373
paddy@57 374 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
paddy@41 375 m.endpointLock.RLock()
paddy@41 376 defer m.endpointLock.RUnlock()
paddy@41 377 return m.endpoints[client.String()], nil
paddy@41 378 }
paddy@54 379
paddy@57 380 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
paddy@54 381 m.endpointLock.RLock()
paddy@54 382 defer m.endpointLock.RUnlock()
paddy@54 383 return int64(len(m.endpoints[client.String()])), nil
paddy@54 384 }
paddy@108 385
paddy@108 386 type newClientReq struct {
paddy@108 387 Name string `json:"name"`
paddy@108 388 Logo string `json:"logo"`
paddy@108 389 Website string `json:"website"`
paddy@108 390 Type string `json:"type"`
paddy@108 391 Endpoints []string `json:"endpoints"`
paddy@108 392 }
paddy@108 393
paddy@108 394 func RegisterClientHandlers(r *mux.Router, context Context) {
paddy@108 395 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
paddy@131 396 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET")
paddy@131 397 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET")
paddy@133 398 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH")
paddy@128 399 // BUG(paddy): We need to implement a handler to delete a client. Also, what should that do with the grants and tokens belonging to that client?
paddy@137 400 r.Handle("/clients/{id}/endpoints", wrap(context, AddEndpointsHandler)).Methods("POST")
paddy@128 401 // BUG(paddy): We need to implement a handler to remove an endpoint from a client.
paddy@138 402 r.Handle("/clients/{id}/endpoints", wrap(context, ListEndpointsHandler)).Methods("GET")
paddy@108 403 }
paddy@108 404
paddy@108 405 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
paddy@115 406 errors := []requestError{}
paddy@108 407 username, password, ok := r.BasicAuth()
paddy@108 408 if !ok {
paddy@115 409 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@115 410 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@108 411 return
paddy@108 412 }
paddy@108 413 profile, err := authenticate(username, password, c)
paddy@108 414 if err != nil {
paddy@139 415 if isAuthError(err) {
paddy@139 416 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@139 417 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@139 418 } else {
paddy@139 419 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@139 420 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@139 421 }
paddy@108 422 return
paddy@108 423 }
paddy@108 424 var req newClientReq
paddy@108 425 decoder := json.NewDecoder(r.Body)
paddy@108 426 err = decoder.Decode(&req)
paddy@108 427 if err != nil {
paddy@108 428 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@108 429 return
paddy@108 430 }
paddy@116 431 if req.Type == "" {
paddy@116 432 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
paddy@116 433 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
paddy@115 434 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
paddy@116 435 }
paddy@116 436 if req.Name == "" {
paddy@116 437 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
paddy@116 438 } else if len(req.Name) < minClientNameLen {
paddy@116 439 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
paddy@116 440 } else if len(req.Name) > maxClientNameLen {
paddy@116 441 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
paddy@116 442 }
paddy@116 443 if len(errors) > 0 {
paddy@115 444 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@108 445 return
paddy@108 446 }
paddy@108 447 client := Client{
paddy@108 448 ID: uuid.NewID(),
paddy@108 449 OwnerID: profile.ID,
paddy@108 450 Name: req.Name,
paddy@108 451 Logo: req.Logo,
paddy@108 452 Website: req.Website,
paddy@108 453 Type: req.Type,
paddy@108 454 }
paddy@118 455 if client.Type == clientTypeConfidential {
paddy@115 456 secret := make([]byte, 32)
paddy@115 457 _, err = rand.Read(secret)
paddy@115 458 if err != nil {
paddy@115 459 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@115 460 return
paddy@115 461 }
paddy@115 462 client.Secret = hex.EncodeToString(secret)
paddy@115 463 }
paddy@108 464 err = c.SaveClient(client)
paddy@108 465 if err != nil {
paddy@115 466 if err == ErrClientAlreadyExists {
paddy@115 467 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
paddy@115 468 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@115 469 return
paddy@115 470 }
paddy@115 471 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@108 472 return
paddy@108 473 }
paddy@108 474 endpoints := []Endpoint{}
paddy@115 475 for pos, u := range req.Endpoints {
paddy@108 476 uri, err := url.Parse(u)
paddy@108 477 if err != nil {
paddy@115 478 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
paddy@108 479 continue
paddy@108 480 }
paddy@116 481 if !uri.IsAbs() {
paddy@116 482 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
paddy@116 483 continue
paddy@116 484 }
paddy@108 485 endpoint := Endpoint{
paddy@108 486 ID: uuid.NewID(),
paddy@108 487 ClientID: client.ID,
paddy@116 488 URI: uri.String(),
paddy@108 489 Added: time.Now(),
paddy@108 490 }
paddy@108 491 endpoints = append(endpoints, endpoint)
paddy@108 492 }
paddy@115 493 err = c.AddEndpoints(client.ID, endpoints)
paddy@115 494 if err != nil {
paddy@115 495 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@115 496 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
paddy@115 497 return
paddy@115 498 }
paddy@108 499 resp := response{
paddy@108 500 Clients: []Client{client},
paddy@108 501 Endpoints: endpoints,
paddy@116 502 Errors: errors,
paddy@108 503 }
paddy@108 504 encode(w, r, http.StatusCreated, resp)
paddy@108 505 }
paddy@121 506
paddy@131 507 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
paddy@131 508 errors := []requestError{}
paddy@131 509 vars := mux.Vars(r)
paddy@131 510 if vars["id"] == "" {
paddy@131 511 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@131 512 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@131 513 return
paddy@131 514 }
paddy@131 515 id, err := uuid.Parse(vars["id"])
paddy@131 516 if err != nil {
paddy@131 517 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
paddy@131 518 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@131 519 return
paddy@131 520 }
paddy@131 521 client, err := c.GetClient(id)
paddy@131 522 if err != nil {
paddy@131 523 if err == ErrClientNotFound {
paddy@131 524 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
paddy@139 525 encode(w, r, http.StatusNotFound, response{Errors: errors})
paddy@131 526 return
paddy@131 527 }
paddy@131 528 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@131 529 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@131 530 return
paddy@131 531 }
paddy@139 532 username, password, ok := r.BasicAuth()
paddy@139 533 if !ok {
paddy@139 534 client.Secret = ""
paddy@139 535 } else {
paddy@139 536 profile, err := authenticate(username, password, c)
paddy@139 537 if err != nil {
paddy@139 538 if isAuthError(err) {
paddy@139 539 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@139 540 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@139 541 } else {
paddy@139 542 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@139 543 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@139 544 }
paddy@139 545 return
paddy@139 546 }
paddy@139 547 if !client.OwnerID.Equal(profile.ID) {
paddy@139 548 client.Secret = ""
paddy@139 549 }
paddy@139 550 }
paddy@131 551 resp := response{
paddy@131 552 Clients: []Client{client},
paddy@131 553 Errors: errors,
paddy@131 554 }
paddy@131 555 encode(w, r, http.StatusOK, resp)
paddy@131 556 }
paddy@131 557
paddy@131 558 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
paddy@131 559 errors := []requestError{}
paddy@131 560 var err error
paddy@131 561 // BUG(paddy): If ids are provided in query params, retrieve only those clients
paddy@131 562 // BUG(paddy): We should have auth when listing clients
paddy@131 563 num := defaultClientResponseSize
paddy@131 564 offset := 0
paddy@131 565 ownerIDStr := r.URL.Query().Get("owner_id")
paddy@131 566 numStr := r.URL.Query().Get("num")
paddy@131 567 offsetStr := r.URL.Query().Get("offset")
paddy@131 568 if numStr != "" {
paddy@131 569 num, err = strconv.Atoi(numStr)
paddy@131 570 if err != nil {
paddy@131 571 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
paddy@131 572 }
paddy@131 573 if num > maxClientResponseSize {
paddy@131 574 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
paddy@131 575 }
paddy@131 576 }
paddy@131 577 if offsetStr != "" {
paddy@131 578 offset, err = strconv.Atoi(offsetStr)
paddy@131 579 if err != nil {
paddy@131 580 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
paddy@131 581 }
paddy@131 582 }
paddy@131 583 if ownerIDStr == "" {
paddy@131 584 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
paddy@131 585 }
paddy@131 586 if len(errors) > 0 {
paddy@131 587 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@131 588 return
paddy@131 589 }
paddy@131 590 ownerID, err := uuid.Parse(ownerIDStr)
paddy@131 591 if err != nil {
paddy@131 592 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
paddy@131 593 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@131 594 return
paddy@131 595 }
paddy@131 596 clients, err := c.ListClientsByOwner(ownerID, num, offset)
paddy@131 597 if err != nil {
paddy@131 598 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@131 599 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@131 600 return
paddy@131 601 }
paddy@131 602 for pos, client := range clients {
paddy@131 603 client.Secret = ""
paddy@131 604 clients[pos] = client
paddy@131 605 }
paddy@131 606 resp := response{
paddy@131 607 Clients: clients,
paddy@131 608 Errors: errors,
paddy@131 609 }
paddy@131 610 encode(w, r, http.StatusOK, resp)
paddy@131 611 }
paddy@131 612
paddy@133 613 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
paddy@133 614 errors := []requestError{}
paddy@133 615 vars := mux.Vars(r)
paddy@133 616 if _, ok := vars["id"]; !ok {
paddy@133 617 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@133 618 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@133 619 return
paddy@133 620 }
paddy@133 621 var change ClientChange
paddy@133 622 err := decode(r, &change)
paddy@133 623 if err != nil {
paddy@133 624 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
paddy@133 625 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@133 626 return
paddy@133 627 }
paddy@133 628 errs := change.Validate()
paddy@133 629 for _, err := range errs {
paddy@133 630 switch err {
paddy@133 631 case ErrEmptyChange:
paddy@133 632 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"})
paddy@133 633 case ErrClientNameTooShort:
paddy@133 634 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
paddy@133 635 case ErrClientNameTooLong:
paddy@133 636 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
paddy@133 637 case ErrClientLogoTooLong:
paddy@133 638 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"})
paddy@133 639 case ErrClientLogoNotURL:
paddy@133 640 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"})
paddy@133 641 case ErrClientWebsiteTooLong:
paddy@133 642 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"})
paddy@133 643 case ErrClientWebsiteNotURL:
paddy@133 644 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"})
paddy@133 645 default:
paddy@133 646 log.Println("Unrecognised error from client change validation:", err)
paddy@133 647 }
paddy@133 648 }
paddy@133 649 id, err := uuid.Parse(vars["id"])
paddy@133 650 if err != nil {
paddy@133 651 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
paddy@133 652 }
paddy@133 653 if len(errors) > 0 {
paddy@133 654 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@133 655 return
paddy@133 656 }
paddy@133 657 client, err := c.GetClient(id)
paddy@133 658 if err == ErrClientNotFound {
paddy@133 659 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
paddy@133 660 encode(w, r, http.StatusNotFound, response{Errors: errors})
paddy@133 661 return
paddy@133 662 } else if err != nil {
paddy@133 663 log.Println("Error retrieving client:", err)
paddy@133 664 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@133 665 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@133 666 return
paddy@133 667 }
paddy@133 668 if change.Secret != nil && client.Type == clientTypeConfidential {
paddy@133 669 secret := make([]byte, 32)
paddy@133 670 _, err = rand.Read(secret)
paddy@133 671 if err != nil {
paddy@133 672 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@133 673 return
paddy@133 674 }
paddy@133 675 newSecret := hex.EncodeToString(secret)
paddy@133 676 change.Secret = &newSecret
paddy@133 677 }
paddy@133 678 err = c.UpdateClient(id, change)
paddy@133 679 if err != nil {
paddy@133 680 log.Println("Error updating client:", err)
paddy@133 681 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@133 682 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@133 683 return
paddy@133 684 }
paddy@133 685 client.ApplyChange(change)
paddy@133 686 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
paddy@133 687 return
paddy@133 688 }
paddy@133 689
paddy@137 690 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
paddy@137 691 type addEndpointReq struct {
paddy@137 692 Endpoints []string `json:"endpoints"`
paddy@137 693 }
paddy@137 694 errors := []requestError{}
paddy@137 695 vars := mux.Vars(r)
paddy@137 696 if vars["id"] == "" {
paddy@137 697 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@137 698 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@137 699 return
paddy@137 700 }
paddy@137 701 id, err := uuid.Parse(vars["id"])
paddy@137 702 if err != nil {
paddy@137 703 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
paddy@137 704 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@137 705 return
paddy@137 706 }
paddy@137 707 _, err = c.GetClient(id)
paddy@137 708 if err != nil {
paddy@137 709 if err == ErrClientNotFound {
paddy@137 710 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
paddy@137 711 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@137 712 return
paddy@137 713 }
paddy@137 714 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@137 715 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@137 716 return
paddy@137 717 }
paddy@137 718 var req addEndpointReq
paddy@137 719 decoder := json.NewDecoder(r.Body)
paddy@137 720 err = decoder.Decode(&req)
paddy@137 721 if err != nil {
paddy@137 722 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@137 723 return
paddy@137 724 }
paddy@137 725 if len(req.Endpoints) < 1 {
paddy@137 726 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"})
paddy@137 727 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@137 728 return
paddy@137 729 }
paddy@137 730 endpoints := []Endpoint{}
paddy@137 731 for pos, u := range req.Endpoints {
paddy@137 732 if parsed, err := url.Parse(u); err != nil {
paddy@137 733 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
paddy@137 734 continue
paddy@137 735 } else if !parsed.IsAbs() {
paddy@137 736 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
paddy@137 737 continue
paddy@137 738 }
paddy@137 739 e := Endpoint{
paddy@137 740 ID: uuid.NewID(),
paddy@137 741 ClientID: id,
paddy@137 742 URI: u,
paddy@137 743 Added: time.Now(),
paddy@137 744 }
paddy@137 745 endpoints = append(endpoints, e)
paddy@137 746 }
paddy@137 747 if len(errors) > 0 {
paddy@137 748 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@137 749 return
paddy@137 750 }
paddy@137 751 err = c.AddEndpoints(id, endpoints)
paddy@137 752 if err != nil {
paddy@137 753 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@137 754 return
paddy@137 755 }
paddy@137 756 resp := response{
paddy@137 757 Errors: errors,
paddy@137 758 Endpoints: endpoints,
paddy@137 759 }
paddy@137 760 encode(w, r, http.StatusCreated, resp)
paddy@137 761 }
paddy@137 762
paddy@138 763 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
paddy@138 764 errors := []requestError{}
paddy@138 765 vars := mux.Vars(r)
paddy@138 766 clientID, err := uuid.Parse(vars["id"])
paddy@138 767 if err != nil {
paddy@138 768 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
paddy@138 769 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@138 770 return
paddy@138 771 }
paddy@138 772 num := defaultEndpointResponseSize
paddy@138 773 offset := 0
paddy@138 774 numStr := r.URL.Query().Get("num")
paddy@138 775 offsetStr := r.URL.Query().Get("offset")
paddy@138 776 if numStr != "" {
paddy@138 777 num, err = strconv.Atoi(numStr)
paddy@138 778 if err != nil {
paddy@138 779 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
paddy@138 780 }
paddy@138 781 if num > maxEndpointResponseSize {
paddy@138 782 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
paddy@138 783 }
paddy@138 784 }
paddy@138 785 if offsetStr != "" {
paddy@138 786 offset, err = strconv.Atoi(offsetStr)
paddy@138 787 if err != nil {
paddy@138 788 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
paddy@138 789 }
paddy@138 790 }
paddy@138 791 if len(errors) > 0 {
paddy@138 792 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@138 793 return
paddy@138 794 }
paddy@138 795 endpoints, err := c.ListEndpoints(clientID, num, offset)
paddy@138 796 if err != nil {
paddy@138 797 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@138 798 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@138 799 return
paddy@138 800 }
paddy@138 801 resp := response{
paddy@138 802 Endpoints: endpoints,
paddy@138 803 Errors: errors,
paddy@138 804 }
paddy@138 805 encode(w, r, http.StatusOK, resp)
paddy@138 806 }
paddy@138 807
paddy@135 808 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
paddy@135 809 scopes = strings.Split(r.PostFormValue("scope"), " ")
paddy@121 810 valid = true
paddy@121 811 return
paddy@121 812 }
paddy@124 813
paddy@124 814 func clientCredentialsAuditString(r *http.Request) string {
paddy@124 815 return "client_credentials"
paddy@124 816 }