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