auth

Paddy 2015-03-21 Parent:8267e1c8bcd1 Child:77db7c65216c

150:379702564771 Go to Latest

auth/client.go

Fix whitespace in Profile queries. The lack of whitespace around the ` = ?` expression would have bothered me, so I fixed it.

History
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")
40 // ErrEndpointNotFound is returned when an Endpoint is requested but not found in a clientSTore.
41 ErrEndpointNotFound = errors.New("endpoint not found in clientStore")
43 // ErrEmptyChange is returned when a Change has all its properties set to nil.
44 ErrEmptyChange = errors.New("change must have at least one property set")
45 // ErrClientNameTooShort is returned when a Client's Name property is too short.
46 ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
47 // ErrClientNameTooLong is returned when a Client's Name property is too long.
48 ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
49 // ErrClientLogoTooLong is returned when a Client's Logo property is too long.
50 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
51 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL.
52 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
53 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long.
54 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
55 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL.
56 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
57 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL.
58 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL")
59 )
61 const (
62 clientTypePublic = "public"
63 clientTypeConfidential = "confidential"
64 minClientNameLen = 2
65 maxClientNameLen = 24
66 defaultClientResponseSize = 20
67 maxClientResponseSize = 50
68 defaultEndpointResponseSize = 20
69 maxEndpointResponseSize = 50
71 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery
72 )
74 // Client represents a client that grants access
75 // to the auth server, exchanging grants for tokens,
76 // and tokens for access.
77 type Client struct {
78 ID uuid.ID `json:"id,omitempty"`
79 Secret string `json:"secret,omitempty"`
80 OwnerID uuid.ID `json:"owner_id,omitempty"`
81 Name string `json:"name,omitempty"`
82 Logo string `json:"logo,omitempty"`
83 Website string `json:"website,omitempty"`
84 Type string `json:"type,omitempty"`
85 }
87 // ApplyChange applies the properties of the passed
88 // ClientChange to the Client object it is called on.
89 func (c *Client) ApplyChange(change ClientChange) {
90 if change.Secret != nil {
91 c.Secret = *change.Secret
92 }
93 if change.OwnerID != nil {
94 c.OwnerID = change.OwnerID
95 }
96 if change.Name != nil {
97 c.Name = *change.Name
98 }
99 if change.Logo != nil {
100 c.Logo = *change.Logo
101 }
102 if change.Website != nil {
103 c.Website = *change.Website
104 }
105 }
107 // ClientChange represents a bundle of options for
108 // updating a Client's mutable data.
109 type ClientChange struct {
110 Secret *string
111 OwnerID uuid.ID
112 Name *string
113 Logo *string
114 Website *string
115 }
117 // Validate checks the ClientChange it is called on
118 // and asserts its internal validity, or lack thereof.
119 func (c ClientChange) Validate() []error {
120 errors := []error{}
121 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
122 errors = append(errors, ErrEmptyChange)
123 return errors
124 }
125 if c.Name != nil && len(*c.Name) < 2 {
126 errors = append(errors, ErrClientNameTooShort)
127 }
128 if c.Name != nil && len(*c.Name) > 32 {
129 errors = append(errors, ErrClientNameTooLong)
130 }
131 if c.Logo != nil && *c.Logo != "" {
132 if len(*c.Logo) > 1024 {
133 errors = append(errors, ErrClientLogoTooLong)
134 }
135 u, err := url.Parse(*c.Logo)
136 if err != nil || !u.IsAbs() {
137 errors = append(errors, ErrClientLogoNotURL)
138 }
139 }
140 if c.Website != nil && *c.Website != "" {
141 if len(*c.Website) > 140 {
142 errors = append(errors, ErrClientWebsiteTooLong)
143 }
144 u, err := url.Parse(*c.Website)
145 if err != nil || !u.IsAbs() {
146 errors = append(errors, ErrClientWebsiteNotURL)
147 }
148 }
149 return errors
150 }
152 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) {
153 enc := json.NewEncoder(w)
154 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
155 if !fromAuthHeader {
156 clientIDStr = r.PostFormValue("client_id")
157 }
158 if clientIDStr == "" {
159 w.WriteHeader(http.StatusUnauthorized)
160 if fromAuthHeader {
161 w.Header().Set("WWW-Authenticate", "Basic")
162 }
163 renderJSONError(enc, "invalid_client")
164 return nil, "", false
165 }
166 if !allowPublic && !fromAuthHeader {
167 w.WriteHeader(http.StatusBadRequest)
168 renderJSONError(enc, "unauthorized_client")
169 return nil, "", false
170 }
171 clientID, err := uuid.Parse(clientIDStr)
172 if err != nil {
173 log.Println("Error decoding client ID:", err)
174 w.WriteHeader(http.StatusUnauthorized)
175 if fromAuthHeader {
176 w.Header().Set("WWW-Authenticate", "Basic")
177 }
178 renderJSONError(enc, "invalid_client")
179 return nil, "", false
180 }
181 return clientID, clientSecret, true
182 }
184 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
185 enc := json.NewEncoder(w)
186 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic)
187 if !ok {
188 return nil, false
189 }
190 _, _, fromAuthHeader := r.BasicAuth()
191 client, err := context.GetClient(clientID)
192 if err == ErrClientNotFound {
193 w.WriteHeader(http.StatusUnauthorized)
194 if fromAuthHeader {
195 w.Header().Set("WWW-Authenticate", "Basic")
196 }
197 renderJSONError(enc, "invalid_client")
198 return nil, false
199 } else if err != nil {
200 w.WriteHeader(http.StatusInternalServerError)
201 renderJSONError(enc, "server_error")
202 return nil, false
203 }
204 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
205 w.WriteHeader(http.StatusUnauthorized)
206 if fromAuthHeader {
207 w.Header().Set("WWW-Authenticate", "Basic")
208 }
209 renderJSONError(enc, "invalid_client")
210 return nil, false
211 }
212 return clientID, true
213 }
215 // Endpoint represents a single URI that a Client
216 // controls. Users will be redirected to these URIs
217 // following successful authorization grants and
218 // exchanges for access tokens.
219 type Endpoint struct {
220 ID uuid.ID `json:"id,omitempty"`
221 ClientID uuid.ID `json:"client_id,omitempty"`
222 URI string `json:"uri,omitempty"`
223 NormalizedURI string `json:"-"`
224 Added time.Time `json:"added,omitempty"`
225 }
227 func normalizeURIString(in string) (string, error) {
228 n, err := purell.NormalizeURLString(in, normalizeFlags)
229 if err != nil {
230 log.Println(err)
231 return in, ErrEndpointURINotURL
232 }
233 return n, nil
234 }
236 func normalizeURI(in *url.URL) string {
237 return purell.NormalizeURL(in, normalizeFlags)
238 }
240 type sortedEndpoints []Endpoint
242 func (s sortedEndpoints) Len() int {
243 return len(s)
244 }
246 func (s sortedEndpoints) Less(i, j int) bool {
247 return s[i].Added.Before(s[j].Added)
248 }
250 func (s sortedEndpoints) Swap(i, j int) {
251 s[i], s[j] = s[j], s[i]
252 }
254 type clientStore interface {
255 getClient(id uuid.ID) (Client, error)
256 saveClient(client Client) error
257 updateClient(id uuid.ID, change ClientChange) error
258 deleteClient(id uuid.ID) error
259 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
261 addEndpoints(client uuid.ID, endpoint []Endpoint) error
262 removeEndpoint(client, endpoint uuid.ID) error
263 getEndpoint(client, endpoint uuid.ID) (Endpoint, error)
264 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
265 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
266 countEndpoints(client uuid.ID) (int64, error)
267 }
269 func (m *memstore) getClient(id uuid.ID) (Client, error) {
270 m.clientLock.RLock()
271 defer m.clientLock.RUnlock()
272 c, ok := m.clients[id.String()]
273 if !ok {
274 return Client{}, ErrClientNotFound
275 }
276 return c, nil
277 }
279 func (m *memstore) saveClient(client Client) error {
280 m.clientLock.Lock()
281 defer m.clientLock.Unlock()
282 if _, ok := m.clients[client.ID.String()]; ok {
283 return ErrClientAlreadyExists
284 }
285 m.clients[client.ID.String()] = client
286 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
287 return nil
288 }
290 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
291 m.clientLock.Lock()
292 defer m.clientLock.Unlock()
293 c, ok := m.clients[id.String()]
294 if !ok {
295 return ErrClientNotFound
296 }
297 c.ApplyChange(change)
298 m.clients[id.String()] = c
299 return nil
300 }
302 func (m *memstore) deleteClient(id uuid.ID) error {
303 client, err := m.getClient(id)
304 if err != nil {
305 return err
306 }
307 m.clientLock.Lock()
308 defer m.clientLock.Unlock()
309 delete(m.clients, id.String())
310 pos := -1
311 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
312 if item.Equal(id) {
313 pos = p
314 break
315 }
316 }
317 if pos >= 0 {
318 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
319 }
320 return nil
321 }
323 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
324 ids := m.lookupClientsByProfileID(ownerID.String())
325 if len(ids) > num+offset {
326 ids = ids[offset : num+offset]
327 } else if len(ids) > offset {
328 ids = ids[offset:]
329 } else {
330 return []Client{}, nil
331 }
332 clients := []Client{}
333 for _, id := range ids {
334 client, err := m.getClient(id)
335 if err != nil {
336 return []Client{}, err
337 }
338 clients = append(clients, client)
339 }
340 return clients, nil
341 }
343 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error {
344 m.endpointLock.Lock()
345 defer m.endpointLock.Unlock()
346 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...)
347 return nil
348 }
350 func (m *memstore) getEndpoint(client, endpoint uuid.ID) (Endpoint, error) {
351 m.endpointLock.Lock()
352 defer m.endpointLock.Unlock()
353 for _, item := range m.endpoints[client.String()] {
354 if item.ID.Equal(endpoint) {
355 return item, nil
356 }
357 }
358 return Endpoint{}, ErrEndpointNotFound
359 }
361 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
362 m.endpointLock.Lock()
363 defer m.endpointLock.Unlock()
364 pos := -1
365 for p, item := range m.endpoints[client.String()] {
366 if item.ID.Equal(endpoint) {
367 pos = p
368 break
369 }
370 }
371 if pos >= 0 {
372 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
373 }
374 return nil
375 }
377 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
378 m.endpointLock.RLock()
379 defer m.endpointLock.RUnlock()
380 for _, candidate := range m.endpoints[client.String()] {
381 if endpoint == candidate.NormalizedURI {
382 return true, nil
383 }
384 }
385 return false, nil
386 }
388 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
389 m.endpointLock.RLock()
390 defer m.endpointLock.RUnlock()
391 return m.endpoints[client.String()], nil
392 }
394 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
395 m.endpointLock.RLock()
396 defer m.endpointLock.RUnlock()
397 return int64(len(m.endpoints[client.String()])), nil
398 }
400 type newClientReq struct {
401 Name string `json:"name"`
402 Logo string `json:"logo"`
403 Website string `json:"website"`
404 Type string `json:"type"`
405 Endpoints []string `json:"endpoints"`
406 }
408 func RegisterClientHandlers(r *mux.Router, context Context) {
409 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
410 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET")
411 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET")
412 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH")
413 r.Handle("/clients/{id}", wrap(context, RemoveClientHandler)).Methods("DELETE")
414 r.Handle("/clients/{id}/endpoints", wrap(context, AddEndpointsHandler)).Methods("POST")
415 r.Handle("/clients/{client_id}/endpoints/{id}", wrap(context, RemoveEndpointHandler)).Methods("DELETE")
416 r.Handle("/clients/{id}/endpoints", wrap(context, ListEndpointsHandler)).Methods("GET")
417 }
419 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
420 errors := []requestError{}
421 username, password, ok := r.BasicAuth()
422 if !ok {
423 errors = append(errors, requestError{Slug: requestErrAccessDenied})
424 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
425 return
426 }
427 profile, err := authenticate(username, password, c)
428 if err != nil {
429 if isAuthError(err) {
430 errors = append(errors, requestError{Slug: requestErrAccessDenied})
431 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
432 } else {
433 log.Printf("Error authenticating: %#+v\n", err)
434 errors = append(errors, requestError{Slug: requestErrActOfGod})
435 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
436 }
437 return
438 }
439 var req newClientReq
440 decoder := json.NewDecoder(r.Body)
441 err = decoder.Decode(&req)
442 if err != nil {
443 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
444 return
445 }
446 if req.Type == "" {
447 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
448 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
449 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
450 }
451 if req.Name == "" {
452 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
453 } else if len(req.Name) < minClientNameLen {
454 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
455 } else if len(req.Name) > maxClientNameLen {
456 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
457 }
458 if len(errors) > 0 {
459 encode(w, r, http.StatusBadRequest, response{Errors: errors})
460 return
461 }
462 client := Client{
463 ID: uuid.NewID(),
464 OwnerID: profile.ID,
465 Name: req.Name,
466 Logo: req.Logo,
467 Website: req.Website,
468 Type: req.Type,
469 }
470 if client.Type == clientTypeConfidential {
471 secret := make([]byte, 32)
472 _, err = rand.Read(secret)
473 if err != nil {
474 log.Printf("Error generating secret: %#+v\n", err)
475 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
476 return
477 }
478 client.Secret = hex.EncodeToString(secret)
479 }
480 err = c.SaveClient(client)
481 if err != nil {
482 if err == ErrClientAlreadyExists {
483 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
484 encode(w, r, http.StatusBadRequest, response{Errors: errors})
485 return
486 }
487 log.Printf("Error saving client: %#+v\n", err)
488 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
489 return
490 }
491 endpoints := []Endpoint{}
492 for pos, u := range req.Endpoints {
493 uri, err := url.Parse(u)
494 if err != nil {
495 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
496 continue
497 }
498 if !uri.IsAbs() {
499 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
500 continue
501 }
502 endpoint := Endpoint{
503 ID: uuid.NewID(),
504 ClientID: client.ID,
505 URI: uri.String(),
506 Added: time.Now(),
507 }
508 endpoints = append(endpoints, endpoint)
509 }
510 err = c.AddEndpoints(client.ID, endpoints)
511 if err != nil {
512 log.Printf("Error adding endpoints: %#+v\n", err)
513 errors = append(errors, requestError{Slug: requestErrActOfGod})
514 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
515 return
516 }
517 resp := response{
518 Clients: []Client{client},
519 Endpoints: endpoints,
520 Errors: errors,
521 }
522 encode(w, r, http.StatusCreated, resp)
523 }
525 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
526 errors := []requestError{}
527 vars := mux.Vars(r)
528 if vars["id"] == "" {
529 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
530 encode(w, r, http.StatusBadRequest, response{Errors: errors})
531 return
532 }
533 id, err := uuid.Parse(vars["id"])
534 if err != nil {
535 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
536 encode(w, r, http.StatusBadRequest, response{Errors: errors})
537 return
538 }
539 client, err := c.GetClient(id)
540 if err != nil {
541 if err == ErrClientNotFound {
542 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
543 encode(w, r, http.StatusNotFound, response{Errors: errors})
544 return
545 }
546 errors = append(errors, requestError{Slug: requestErrActOfGod})
547 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
548 return
549 }
550 username, password, ok := r.BasicAuth()
551 if !ok {
552 client.Secret = ""
553 } else {
554 profile, err := authenticate(username, password, c)
555 if err != nil {
556 if isAuthError(err) {
557 errors = append(errors, requestError{Slug: requestErrAccessDenied})
558 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
559 } else {
560 errors = append(errors, requestError{Slug: requestErrActOfGod})
561 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
562 }
563 return
564 }
565 if !client.OwnerID.Equal(profile.ID) {
566 client.Secret = ""
567 }
568 }
569 resp := response{
570 Clients: []Client{client},
571 Errors: errors,
572 }
573 encode(w, r, http.StatusOK, resp)
574 }
576 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
577 errors := []requestError{}
578 var err error
579 // BUG(paddy): If ids are provided in query params, retrieve only those clients
580 num := defaultClientResponseSize
581 offset := 0
582 ownerIDStr := r.URL.Query().Get("owner_id")
583 numStr := r.URL.Query().Get("num")
584 offsetStr := r.URL.Query().Get("offset")
585 if numStr != "" {
586 num, err = strconv.Atoi(numStr)
587 if err != nil {
588 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
589 }
590 if num > maxClientResponseSize {
591 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
592 }
593 }
594 if offsetStr != "" {
595 offset, err = strconv.Atoi(offsetStr)
596 if err != nil {
597 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
598 }
599 }
600 if ownerIDStr == "" {
601 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
602 }
603 if len(errors) > 0 {
604 encode(w, r, http.StatusBadRequest, response{Errors: errors})
605 return
606 }
607 ownerID, err := uuid.Parse(ownerIDStr)
608 if err != nil {
609 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
610 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
611 return
612 }
613 clients, err := c.ListClientsByOwner(ownerID, num, offset)
614 if err != nil {
615 errors = append(errors, requestError{Slug: requestErrActOfGod})
616 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
617 return
618 }
619 username, password, ok := r.BasicAuth()
620 if !ok {
621 for pos, client := range clients {
622 client.Secret = ""
623 clients[pos] = client
624 }
625 } else {
626 profile, err := authenticate(username, password, c)
627 if err != nil {
628 if isAuthError(err) {
629 errors = append(errors, requestError{Slug: requestErrAccessDenied})
630 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
631 } else {
632 errors = append(errors, requestError{Slug: requestErrActOfGod})
633 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
634 }
635 return
636 }
637 for pos, client := range clients {
638 if !client.OwnerID.Equal(profile.ID) {
639 client.Secret = ""
640 clients[pos] = client
641 }
642 }
643 }
644 resp := response{
645 Clients: clients,
646 Errors: errors,
647 }
648 encode(w, r, http.StatusOK, resp)
649 }
651 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
652 errors := []requestError{}
653 vars := mux.Vars(r)
654 if _, ok := vars["id"]; !ok {
655 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
656 encode(w, r, http.StatusBadRequest, response{Errors: errors})
657 return
658 }
659 id, err := uuid.Parse(vars["id"])
660 if err != nil {
661 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
662 }
663 username, password, ok := r.BasicAuth()
664 if !ok {
665 errors = append(errors, requestError{Slug: requestErrAccessDenied})
666 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
667 return
668 }
669 profile, err := authenticate(username, password, c)
670 if err != nil {
671 if isAuthError(err) {
672 errors = append(errors, requestError{Slug: requestErrAccessDenied})
673 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
674 } else {
675 errors = append(errors, requestError{Slug: requestErrActOfGod})
676 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
677 }
678 return
679 }
680 var change ClientChange
681 err = decode(r, &change)
682 if err != nil {
683 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
684 encode(w, r, http.StatusBadRequest, response{Errors: errors})
685 return
686 }
687 errs := change.Validate()
688 for _, err := range errs {
689 switch err {
690 case ErrEmptyChange:
691 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"})
692 case ErrClientNameTooShort:
693 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
694 case ErrClientNameTooLong:
695 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
696 case ErrClientLogoTooLong:
697 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"})
698 case ErrClientLogoNotURL:
699 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"})
700 case ErrClientWebsiteTooLong:
701 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"})
702 case ErrClientWebsiteNotURL:
703 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"})
704 default:
705 log.Println("Unrecognised error from client change validation:", err)
706 }
707 }
708 if len(errors) > 0 {
709 encode(w, r, http.StatusBadRequest, response{Errors: errors})
710 return
711 }
712 client, err := c.GetClient(id)
713 if err == ErrClientNotFound {
714 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
715 encode(w, r, http.StatusNotFound, response{Errors: errors})
716 return
717 } else if err != nil {
718 log.Println("Error retrieving client:", err)
719 errors = append(errors, requestError{Slug: requestErrActOfGod})
720 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
721 return
722 }
723 if !client.OwnerID.Equal(profile.ID) {
724 errors = append(errors, requestError{Slug: requestErrAccessDenied})
725 encode(w, r, http.StatusForbidden, response{Errors: errors})
726 return
727 }
728 if change.Secret != nil && client.Type == clientTypeConfidential {
729 secret := make([]byte, 32)
730 _, err = rand.Read(secret)
731 if err != nil {
732 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
733 return
734 }
735 newSecret := hex.EncodeToString(secret)
736 change.Secret = &newSecret
737 }
738 err = c.UpdateClient(id, change)
739 if err != nil {
740 log.Println("Error updating client:", err)
741 errors = append(errors, requestError{Slug: requestErrActOfGod})
742 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
743 return
744 }
745 client.ApplyChange(change)
746 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
747 return
748 }
750 func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
751 errors := []requestError{}
752 vars := mux.Vars(r)
753 if _, ok := vars["id"]; !ok {
754 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
755 encode(w, r, http.StatusNotFound, response{Errors: errors})
756 return
757 }
758 id, err := uuid.Parse(vars["id"])
759 if err != nil {
760 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
761 }
762 username, password, ok := r.BasicAuth()
763 if !ok {
764 errors = append(errors, requestError{Slug: requestErrAccessDenied})
765 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
766 return
767 }
768 profile, err := authenticate(username, password, c)
769 if err != nil {
770 if isAuthError(err) {
771 errors = append(errors, requestError{Slug: requestErrAccessDenied})
772 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
773 } else {
774 errors = append(errors, requestError{Slug: requestErrActOfGod})
775 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
776 }
777 return
778 }
779 client, err := c.GetClient(id)
780 if err != nil {
781 if err == ErrClientNotFound {
782 errors = append(errors, requestError{Slug: requestErrNotFound})
783 encode(w, r, http.StatusNotFound, response{Errors: errors})
784 return
785 }
786 log.Println("Error retrieving client:", err)
787 errors = append(errors, requestError{Slug: requestErrActOfGod})
788 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
789 return
790 }
791 if !client.OwnerID.Equal(profile.ID) {
792 errors = append(errors, requestError{Slug: requestErrAccessDenied})
793 encode(w, r, http.StatusForbidden, response{Errors: errors})
794 return
795 }
796 err = c.DeleteClient(id)
797 if err != nil {
798 if err == ErrClientNotFound {
799 errors = append(errors, requestError{Slug: requestErrNotFound})
800 encode(w, r, http.StatusNotFound, response{Errors: errors})
801 return
802 }
803 log.Println("Error deleting client:", err)
804 errors = append(errors, requestError{Slug: requestErrActOfGod})
805 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
806 return
807 }
808 // BUG(paddy): Client needs to clean up after itself, invalidating tokens, deleting unused grants, deleting endpoints
809 encode(w, r, http.StatusOK, response{Errors: errors})
810 return
811 }
813 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
814 type addEndpointReq struct {
815 Endpoints []string `json:"endpoints"`
816 }
817 errors := []requestError{}
818 vars := mux.Vars(r)
819 if vars["id"] == "" {
820 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
821 encode(w, r, http.StatusBadRequest, response{Errors: errors})
822 return
823 }
824 id, err := uuid.Parse(vars["id"])
825 if err != nil {
826 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
827 encode(w, r, http.StatusBadRequest, response{Errors: errors})
828 return
829 }
830 username, password, ok := r.BasicAuth()
831 if !ok {
832 errors = append(errors, requestError{Slug: requestErrAccessDenied})
833 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
834 return
835 }
836 profile, err := authenticate(username, password, c)
837 if err != nil {
838 if isAuthError(err) {
839 errors = append(errors, requestError{Slug: requestErrAccessDenied})
840 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
841 } else {
842 errors = append(errors, requestError{Slug: requestErrActOfGod})
843 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
844 }
845 return
846 }
847 client, err := c.GetClient(id)
848 if err != nil {
849 if err == ErrClientNotFound {
850 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
851 encode(w, r, http.StatusBadRequest, response{Errors: errors})
852 return
853 }
854 errors = append(errors, requestError{Slug: requestErrActOfGod})
855 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
856 return
857 }
858 if !client.OwnerID.Equal(profile.ID) {
859 errors = append(errors, requestError{Slug: requestErrAccessDenied})
860 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
861 return
862 }
863 var req addEndpointReq
864 decoder := json.NewDecoder(r.Body)
865 err = decoder.Decode(&req)
866 if err != nil {
867 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
868 return
869 }
870 if len(req.Endpoints) < 1 {
871 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"})
872 encode(w, r, http.StatusBadRequest, response{Errors: errors})
873 return
874 }
875 endpoints := []Endpoint{}
876 for pos, u := range req.Endpoints {
877 if parsed, err := url.Parse(u); err != nil {
878 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
879 continue
880 } else if !parsed.IsAbs() {
881 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
882 continue
883 }
884 e := Endpoint{
885 ID: uuid.NewID(),
886 ClientID: id,
887 URI: u,
888 Added: time.Now(),
889 }
890 endpoints = append(endpoints, e)
891 }
892 if len(errors) > 0 {
893 encode(w, r, http.StatusBadRequest, response{Errors: errors})
894 return
895 }
896 err = c.AddEndpoints(id, endpoints)
897 if err != nil {
898 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
899 return
900 }
901 resp := response{
902 Errors: errors,
903 Endpoints: endpoints,
904 }
905 encode(w, r, http.StatusCreated, resp)
906 }
908 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
909 errors := []requestError{}
910 vars := mux.Vars(r)
911 clientID, err := uuid.Parse(vars["id"])
912 if err != nil {
913 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
914 encode(w, r, http.StatusBadRequest, response{Errors: errors})
915 return
916 }
917 num := defaultEndpointResponseSize
918 offset := 0
919 numStr := r.URL.Query().Get("num")
920 offsetStr := r.URL.Query().Get("offset")
921 if numStr != "" {
922 num, err = strconv.Atoi(numStr)
923 if err != nil {
924 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
925 }
926 if num > maxEndpointResponseSize {
927 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
928 }
929 }
930 if offsetStr != "" {
931 offset, err = strconv.Atoi(offsetStr)
932 if err != nil {
933 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
934 }
935 }
936 if len(errors) > 0 {
937 encode(w, r, http.StatusBadRequest, response{Errors: errors})
938 return
939 }
940 endpoints, err := c.ListEndpoints(clientID, num, offset)
941 if err != nil {
942 errors = append(errors, requestError{Slug: requestErrActOfGod})
943 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
944 return
945 }
946 resp := response{
947 Endpoints: endpoints,
948 Errors: errors,
949 }
950 encode(w, r, http.StatusOK, resp)
951 }
953 func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) {
954 errors := []requestError{}
955 vars := mux.Vars(r)
956 if vars["client_id"] == "" {
957 errors = append(errors, requestError{Slug: requestErrMissing, Param: "client_id"})
958 encode(w, r, http.StatusBadRequest, response{Errors: errors})
959 return
960 }
961 clientID, err := uuid.Parse(vars["client_id"])
962 if err != nil {
963 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
964 encode(w, r, http.StatusBadRequest, response{Errors: errors})
965 return
966 }
967 if vars["id"] == "" {
968 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
969 encode(w, r, http.StatusBadRequest, response{Errors: errors})
970 return
971 }
972 id, err := uuid.Parse(vars["id"])
973 if err != nil {
974 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
975 encode(w, r, http.StatusBadRequest, response{Errors: errors})
976 return
977 }
978 username, password, ok := r.BasicAuth()
979 if !ok {
980 errors = append(errors, requestError{Slug: requestErrAccessDenied})
981 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
982 return
983 }
984 profile, err := authenticate(username, password, c)
985 if err != nil {
986 if isAuthError(err) {
987 errors = append(errors, requestError{Slug: requestErrAccessDenied})
988 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
989 } else {
990 errors = append(errors, requestError{Slug: requestErrActOfGod})
991 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
992 }
993 return
994 }
995 client, err := c.GetClient(clientID)
996 if err != nil {
997 if err == ErrClientNotFound {
998 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "client_id"})
999 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1000 return
1002 errors = append(errors, requestError{Slug: requestErrActOfGod})
1003 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1004 return
1006 if !client.OwnerID.Equal(profile.ID) {
1007 errors = append(errors, requestError{Slug: requestErrAccessDenied})
1008 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
1009 return
1011 endpoint, err := c.GetEndpoint(clientID, id)
1012 if err != nil {
1013 if err == ErrEndpointNotFound {
1014 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
1015 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1016 return
1018 errors = append(errors, requestError{Slug: requestErrActOfGod})
1019 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1020 return
1022 err = c.RemoveEndpoint(clientID, id)
1023 if err != nil {
1024 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
1025 return
1027 resp := response{
1028 Errors: errors,
1029 Endpoints: []Endpoint{endpoint},
1031 encode(w, r, http.StatusCreated, resp)
1034 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
1035 scopes = strings.Split(r.PostFormValue("scope"), " ")
1036 valid = true
1037 return
1040 func clientCredentialsAuditString(r *http.Request) string {
1041 return "client_credentials"