auth

Paddy 2015-01-29 Parent:f474ce964dcf Child:d30a3a12d387

133:bc842183181d Go to Latest

auth/client.go

Add Client updating from the API. Add a handler to update Clients using the API. Add a helper that will decode a request for us based on its Content-Type header. Change the ClientChange.Validate function to return as many errors as possible, as opposed to just the first error it encounters. Update the ClientChange.Validate tests to take advantage of the new signature.

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