auth
auth/client.go
Do a first, naive pass at storing profiles in Postgres. This is untested against an actual database. It's a best-guess attempt at SQL. It _should_ work. I think. Start storing things in Postgres, starting with Profiles and Logins. This necessitates the addition of a Deleted property to the Profile type, because I'm not deleting those in case of accidental deletion. Logins, though, we'll delete. This also necessitates updating the profileStore interface to no longer have a deleteProfile method, because we're tracking that through updates now. Then we need to update our profileStore tests, because they no longer clean up after themselves. Which, come to think of it, may cause some problems later.
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 errors = append(errors, requestError{Slug: requestErrActOfGod})
434 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
435 }
436 return
437 }
438 var req newClientReq
439 decoder := json.NewDecoder(r.Body)
440 err = decoder.Decode(&req)
441 if err != nil {
442 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
443 return
444 }
445 if req.Type == "" {
446 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
447 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
448 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
449 }
450 if req.Name == "" {
451 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
452 } else if len(req.Name) < minClientNameLen {
453 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
454 } else if len(req.Name) > maxClientNameLen {
455 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
456 }
457 if len(errors) > 0 {
458 encode(w, r, http.StatusBadRequest, response{Errors: errors})
459 return
460 }
461 client := Client{
462 ID: uuid.NewID(),
463 OwnerID: profile.ID,
464 Name: req.Name,
465 Logo: req.Logo,
466 Website: req.Website,
467 Type: req.Type,
468 }
469 if client.Type == clientTypeConfidential {
470 secret := make([]byte, 32)
471 _, err = rand.Read(secret)
472 if err != nil {
473 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
474 return
475 }
476 client.Secret = hex.EncodeToString(secret)
477 }
478 err = c.SaveClient(client)
479 if err != nil {
480 if err == ErrClientAlreadyExists {
481 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
482 encode(w, r, http.StatusBadRequest, response{Errors: errors})
483 return
484 }
485 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
486 return
487 }
488 endpoints := []Endpoint{}
489 for pos, u := range req.Endpoints {
490 uri, err := url.Parse(u)
491 if err != nil {
492 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
493 continue
494 }
495 if !uri.IsAbs() {
496 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
497 continue
498 }
499 endpoint := Endpoint{
500 ID: uuid.NewID(),
501 ClientID: client.ID,
502 URI: uri.String(),
503 Added: time.Now(),
504 }
505 endpoints = append(endpoints, endpoint)
506 }
507 err = c.AddEndpoints(client.ID, endpoints)
508 if err != nil {
509 errors = append(errors, requestError{Slug: requestErrActOfGod})
510 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
511 return
512 }
513 resp := response{
514 Clients: []Client{client},
515 Endpoints: endpoints,
516 Errors: errors,
517 }
518 encode(w, r, http.StatusCreated, resp)
519 }
521 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
522 errors := []requestError{}
523 vars := mux.Vars(r)
524 if vars["id"] == "" {
525 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
526 encode(w, r, http.StatusBadRequest, response{Errors: errors})
527 return
528 }
529 id, err := uuid.Parse(vars["id"])
530 if err != nil {
531 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
532 encode(w, r, http.StatusBadRequest, response{Errors: errors})
533 return
534 }
535 client, err := c.GetClient(id)
536 if err != nil {
537 if err == ErrClientNotFound {
538 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
539 encode(w, r, http.StatusNotFound, response{Errors: errors})
540 return
541 }
542 errors = append(errors, requestError{Slug: requestErrActOfGod})
543 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
544 return
545 }
546 username, password, ok := r.BasicAuth()
547 if !ok {
548 client.Secret = ""
549 } else {
550 profile, err := authenticate(username, password, c)
551 if err != nil {
552 if isAuthError(err) {
553 errors = append(errors, requestError{Slug: requestErrAccessDenied})
554 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
555 } else {
556 errors = append(errors, requestError{Slug: requestErrActOfGod})
557 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
558 }
559 return
560 }
561 if !client.OwnerID.Equal(profile.ID) {
562 client.Secret = ""
563 }
564 }
565 resp := response{
566 Clients: []Client{client},
567 Errors: errors,
568 }
569 encode(w, r, http.StatusOK, resp)
570 }
572 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
573 errors := []requestError{}
574 var err error
575 // BUG(paddy): If ids are provided in query params, retrieve only those clients
576 num := defaultClientResponseSize
577 offset := 0
578 ownerIDStr := r.URL.Query().Get("owner_id")
579 numStr := r.URL.Query().Get("num")
580 offsetStr := r.URL.Query().Get("offset")
581 if numStr != "" {
582 num, err = strconv.Atoi(numStr)
583 if err != nil {
584 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
585 }
586 if num > maxClientResponseSize {
587 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
588 }
589 }
590 if offsetStr != "" {
591 offset, err = strconv.Atoi(offsetStr)
592 if err != nil {
593 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
594 }
595 }
596 if ownerIDStr == "" {
597 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
598 }
599 if len(errors) > 0 {
600 encode(w, r, http.StatusBadRequest, response{Errors: errors})
601 return
602 }
603 ownerID, err := uuid.Parse(ownerIDStr)
604 if err != nil {
605 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
606 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
607 return
608 }
609 clients, err := c.ListClientsByOwner(ownerID, num, offset)
610 if err != nil {
611 errors = append(errors, requestError{Slug: requestErrActOfGod})
612 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
613 return
614 }
615 username, password, ok := r.BasicAuth()
616 if !ok {
617 for pos, client := range clients {
618 client.Secret = ""
619 clients[pos] = client
620 }
621 } else {
622 profile, err := authenticate(username, password, c)
623 if err != nil {
624 if isAuthError(err) {
625 errors = append(errors, requestError{Slug: requestErrAccessDenied})
626 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
627 } else {
628 errors = append(errors, requestError{Slug: requestErrActOfGod})
629 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
630 }
631 return
632 }
633 for pos, client := range clients {
634 if !client.OwnerID.Equal(profile.ID) {
635 client.Secret = ""
636 clients[pos] = client
637 }
638 }
639 }
640 resp := response{
641 Clients: clients,
642 Errors: errors,
643 }
644 encode(w, r, http.StatusOK, resp)
645 }
647 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
648 errors := []requestError{}
649 vars := mux.Vars(r)
650 if _, ok := vars["id"]; !ok {
651 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
652 encode(w, r, http.StatusBadRequest, response{Errors: errors})
653 return
654 }
655 id, err := uuid.Parse(vars["id"])
656 if err != nil {
657 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
658 }
659 username, password, ok := r.BasicAuth()
660 if !ok {
661 errors = append(errors, requestError{Slug: requestErrAccessDenied})
662 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
663 return
664 }
665 profile, err := authenticate(username, password, c)
666 if err != nil {
667 if isAuthError(err) {
668 errors = append(errors, requestError{Slug: requestErrAccessDenied})
669 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
670 } else {
671 errors = append(errors, requestError{Slug: requestErrActOfGod})
672 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
673 }
674 return
675 }
676 var change ClientChange
677 err = decode(r, &change)
678 if err != nil {
679 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
680 encode(w, r, http.StatusBadRequest, response{Errors: errors})
681 return
682 }
683 errs := change.Validate()
684 for _, err := range errs {
685 switch err {
686 case ErrEmptyChange:
687 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"})
688 case ErrClientNameTooShort:
689 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
690 case ErrClientNameTooLong:
691 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
692 case ErrClientLogoTooLong:
693 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"})
694 case ErrClientLogoNotURL:
695 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"})
696 case ErrClientWebsiteTooLong:
697 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"})
698 case ErrClientWebsiteNotURL:
699 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"})
700 default:
701 log.Println("Unrecognised error from client change validation:", err)
702 }
703 }
704 if len(errors) > 0 {
705 encode(w, r, http.StatusBadRequest, response{Errors: errors})
706 return
707 }
708 client, err := c.GetClient(id)
709 if err == ErrClientNotFound {
710 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
711 encode(w, r, http.StatusNotFound, response{Errors: errors})
712 return
713 } else if err != nil {
714 log.Println("Error retrieving client:", err)
715 errors = append(errors, requestError{Slug: requestErrActOfGod})
716 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
717 return
718 }
719 if !client.OwnerID.Equal(profile.ID) {
720 errors = append(errors, requestError{Slug: requestErrAccessDenied})
721 encode(w, r, http.StatusForbidden, response{Errors: errors})
722 return
723 }
724 if change.Secret != nil && client.Type == clientTypeConfidential {
725 secret := make([]byte, 32)
726 _, err = rand.Read(secret)
727 if err != nil {
728 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
729 return
730 }
731 newSecret := hex.EncodeToString(secret)
732 change.Secret = &newSecret
733 }
734 err = c.UpdateClient(id, change)
735 if err != nil {
736 log.Println("Error updating client:", err)
737 errors = append(errors, requestError{Slug: requestErrActOfGod})
738 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
739 return
740 }
741 client.ApplyChange(change)
742 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
743 return
744 }
746 func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
747 errors := []requestError{}
748 vars := mux.Vars(r)
749 if _, ok := vars["id"]; !ok {
750 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
751 encode(w, r, http.StatusNotFound, response{Errors: errors})
752 return
753 }
754 id, err := uuid.Parse(vars["id"])
755 if err != nil {
756 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
757 }
758 username, password, ok := r.BasicAuth()
759 if !ok {
760 errors = append(errors, requestError{Slug: requestErrAccessDenied})
761 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
762 return
763 }
764 profile, err := authenticate(username, password, c)
765 if err != nil {
766 if isAuthError(err) {
767 errors = append(errors, requestError{Slug: requestErrAccessDenied})
768 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
769 } else {
770 errors = append(errors, requestError{Slug: requestErrActOfGod})
771 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
772 }
773 return
774 }
775 client, err := c.GetClient(id)
776 if err != nil {
777 if err == ErrClientNotFound {
778 errors = append(errors, requestError{Slug: requestErrNotFound})
779 encode(w, r, http.StatusNotFound, response{Errors: errors})
780 return
781 }
782 log.Println("Error retrieving client:", err)
783 errors = append(errors, requestError{Slug: requestErrActOfGod})
784 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
785 return
786 }
787 if !client.OwnerID.Equal(profile.ID) {
788 errors = append(errors, requestError{Slug: requestErrAccessDenied})
789 encode(w, r, http.StatusForbidden, response{Errors: errors})
790 return
791 }
792 err = c.DeleteClient(id)
793 if err != nil {
794 if err == ErrClientNotFound {
795 errors = append(errors, requestError{Slug: requestErrNotFound})
796 encode(w, r, http.StatusNotFound, response{Errors: errors})
797 return
798 }
799 log.Println("Error deleting client:", err)
800 errors = append(errors, requestError{Slug: requestErrActOfGod})
801 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
802 return
803 }
804 // BUG(paddy): Client needs to clean up after itself, invalidating tokens, deleting unused grants, deleting endpoints
805 encode(w, r, http.StatusOK, response{Errors: errors})
806 return
807 }
809 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
810 type addEndpointReq struct {
811 Endpoints []string `json:"endpoints"`
812 }
813 errors := []requestError{}
814 vars := mux.Vars(r)
815 if vars["id"] == "" {
816 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
817 encode(w, r, http.StatusBadRequest, response{Errors: errors})
818 return
819 }
820 id, err := uuid.Parse(vars["id"])
821 if err != nil {
822 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
823 encode(w, r, http.StatusBadRequest, response{Errors: errors})
824 return
825 }
826 username, password, ok := r.BasicAuth()
827 if !ok {
828 errors = append(errors, requestError{Slug: requestErrAccessDenied})
829 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
830 return
831 }
832 profile, err := authenticate(username, password, c)
833 if err != nil {
834 if isAuthError(err) {
835 errors = append(errors, requestError{Slug: requestErrAccessDenied})
836 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
837 } else {
838 errors = append(errors, requestError{Slug: requestErrActOfGod})
839 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
840 }
841 return
842 }
843 client, err := c.GetClient(id)
844 if err != nil {
845 if err == ErrClientNotFound {
846 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
847 encode(w, r, http.StatusBadRequest, response{Errors: errors})
848 return
849 }
850 errors = append(errors, requestError{Slug: requestErrActOfGod})
851 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
852 return
853 }
854 if !client.OwnerID.Equal(profile.ID) {
855 errors = append(errors, requestError{Slug: requestErrAccessDenied})
856 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
857 return
858 }
859 var req addEndpointReq
860 decoder := json.NewDecoder(r.Body)
861 err = decoder.Decode(&req)
862 if err != nil {
863 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
864 return
865 }
866 if len(req.Endpoints) < 1 {
867 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"})
868 encode(w, r, http.StatusBadRequest, response{Errors: errors})
869 return
870 }
871 endpoints := []Endpoint{}
872 for pos, u := range req.Endpoints {
873 if parsed, err := url.Parse(u); err != nil {
874 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
875 continue
876 } else if !parsed.IsAbs() {
877 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
878 continue
879 }
880 e := Endpoint{
881 ID: uuid.NewID(),
882 ClientID: id,
883 URI: u,
884 Added: time.Now(),
885 }
886 endpoints = append(endpoints, e)
887 }
888 if len(errors) > 0 {
889 encode(w, r, http.StatusBadRequest, response{Errors: errors})
890 return
891 }
892 err = c.AddEndpoints(id, endpoints)
893 if err != nil {
894 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
895 return
896 }
897 resp := response{
898 Errors: errors,
899 Endpoints: endpoints,
900 }
901 encode(w, r, http.StatusCreated, resp)
902 }
904 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
905 errors := []requestError{}
906 vars := mux.Vars(r)
907 clientID, err := uuid.Parse(vars["id"])
908 if err != nil {
909 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
910 encode(w, r, http.StatusBadRequest, response{Errors: errors})
911 return
912 }
913 num := defaultEndpointResponseSize
914 offset := 0
915 numStr := r.URL.Query().Get("num")
916 offsetStr := r.URL.Query().Get("offset")
917 if numStr != "" {
918 num, err = strconv.Atoi(numStr)
919 if err != nil {
920 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
921 }
922 if num > maxEndpointResponseSize {
923 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
924 }
925 }
926 if offsetStr != "" {
927 offset, err = strconv.Atoi(offsetStr)
928 if err != nil {
929 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
930 }
931 }
932 if len(errors) > 0 {
933 encode(w, r, http.StatusBadRequest, response{Errors: errors})
934 return
935 }
936 endpoints, err := c.ListEndpoints(clientID, num, offset)
937 if err != nil {
938 errors = append(errors, requestError{Slug: requestErrActOfGod})
939 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
940 return
941 }
942 resp := response{
943 Endpoints: endpoints,
944 Errors: errors,
945 }
946 encode(w, r, http.StatusOK, resp)
947 }
949 func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) {
950 errors := []requestError{}
951 vars := mux.Vars(r)
952 if vars["client_id"] == "" {
953 errors = append(errors, requestError{Slug: requestErrMissing, Param: "client_id"})
954 encode(w, r, http.StatusBadRequest, response{Errors: errors})
955 return
956 }
957 clientID, err := uuid.Parse(vars["client_id"])
958 if err != nil {
959 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
960 encode(w, r, http.StatusBadRequest, response{Errors: errors})
961 return
962 }
963 if vars["id"] == "" {
964 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
965 encode(w, r, http.StatusBadRequest, response{Errors: errors})
966 return
967 }
968 id, err := uuid.Parse(vars["id"])
969 if err != nil {
970 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
971 encode(w, r, http.StatusBadRequest, response{Errors: errors})
972 return
973 }
974 username, password, ok := r.BasicAuth()
975 if !ok {
976 errors = append(errors, requestError{Slug: requestErrAccessDenied})
977 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
978 return
979 }
980 profile, err := authenticate(username, password, c)
981 if err != nil {
982 if isAuthError(err) {
983 errors = append(errors, requestError{Slug: requestErrAccessDenied})
984 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
985 } else {
986 errors = append(errors, requestError{Slug: requestErrActOfGod})
987 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
988 }
989 return
990 }
991 client, err := c.GetClient(clientID)
992 if err != nil {
993 if err == ErrClientNotFound {
994 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "client_id"})
995 encode(w, r, http.StatusBadRequest, response{Errors: errors})
996 return
997 }
998 errors = append(errors, requestError{Slug: requestErrActOfGod})
999 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1000 return
1001 }
1002 if !client.OwnerID.Equal(profile.ID) {
1003 errors = append(errors, requestError{Slug: requestErrAccessDenied})
1004 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
1005 return
1006 }
1007 endpoint, err := c.GetEndpoint(clientID, id)
1008 if err != nil {
1009 if err == ErrEndpointNotFound {
1010 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
1011 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1012 return
1013 }
1014 errors = append(errors, requestError{Slug: requestErrActOfGod})
1015 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1016 return
1017 }
1018 err = c.RemoveEndpoint(clientID, id)
1019 if err != nil {
1020 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
1021 return
1022 }
1023 resp := response{
1024 Errors: errors,
1025 Endpoints: []Endpoint{endpoint},
1026 }
1027 encode(w, r, http.StatusCreated, resp)
1028 }
1030 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
1031 scopes = strings.Split(r.PostFormValue("scope"), " ")
1032 valid = true
1033 return
1034 }
1036 func clientCredentialsAuditString(r *http.Request) string {
1037 return "client_credentials"
1038 }