Stop soft-deleting Profiles and actually delete them.
The information we're storing in Profiles isn't unique enough that we should go
through the hassle we're going through to soft-delete it.
Add a deleteProfile method to our profileStore, and implement it for our
postgres and memstore implementations.
Add a DeleteProfile wrapper for our Context.
Remove the Deleted property from the Profile type and the ProfileChange type,
and update references to it.
Stop cleaning up after our Profile in the UpdateProfileHandler, because there's
no longer any way to delete the Profile from the UpdateProfileHandler.
Update our get/list* methods so they don't filter on the non-existent Deleted
property anymore.
Update our SQL schema definition to not include the deleted column.
Update our profile tests to use the DeleteProfile method and stop comparing the
no-longer-existing Deleted property.
15 "github.com/PuerkitoBio/purell"
16 "github.com/gorilla/mux"
18 "code.secondbit.org/uuid.hg"
22 RegisterGrantType("client_credentials", GrantType{
23 Validate: clientCredentialsValidate,
26 ReturnToken: RenderJSONToken,
28 AuditString: clientCredentialsAuditString,
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")
42 // ErrEndpointAlreadyExists is returned when an Endpoint is added to a clientStore, but another Endpoint
43 // with the same ID already exists in the clientStore.
44 ErrEndpointAlreadyExists = errors.New("endpoint already exists in clientStore")
46 // ErrEmptyChange is returned when a Change has all its properties set to nil.
47 ErrEmptyChange = errors.New("change must have at least one property set")
48 // ErrClientNameTooShort is returned when a Client's Name property is too short.
49 ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
50 // ErrClientNameTooLong is returned when a Client's Name property is too long.
51 ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
52 // ErrClientLogoTooLong is returned when a Client's Logo property is too long.
53 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
54 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL.
55 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
56 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long.
57 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
58 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL.
59 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
60 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL.
61 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL")
65 clientTypePublic = "public"
66 clientTypeConfidential = "confidential"
69 defaultClientResponseSize = 20
70 maxClientResponseSize = 50
71 defaultEndpointResponseSize = 20
72 maxEndpointResponseSize = 50
74 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery
77 // Client represents a client that grants access
78 // to the auth server, exchanging grants for tokens,
79 // and tokens for access.
81 ID uuid.ID `json:"id,omitempty"`
82 Secret string `json:"secret,omitempty"`
83 OwnerID uuid.ID `json:"owner_id,omitempty"`
84 Name string `json:"name,omitempty"`
85 Logo string `json:"logo,omitempty"`
86 Website string `json:"website,omitempty"`
87 Type string `json:"type,omitempty"`
88 Deleted bool `json:"deleted,omitempty"`
91 // ApplyChange applies the properties of the passed
92 // ClientChange to the Client object it is called on.
93 func (c *Client) ApplyChange(change ClientChange) {
94 if change.Secret != nil {
95 c.Secret = *change.Secret
97 if change.OwnerID != nil {
98 c.OwnerID = change.OwnerID
100 if change.Name != nil {
101 c.Name = *change.Name
103 if change.Logo != nil {
104 c.Logo = *change.Logo
106 if change.Website != nil {
107 c.Website = *change.Website
109 if change.Deleted != nil {
110 c.Deleted = *change.Deleted
114 // ClientChange represents a bundle of options for
115 // updating a Client's mutable data.
116 type ClientChange struct {
125 func (c ClientChange) Empty() bool {
126 return c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil && c.Deleted == nil
129 // Validate checks the ClientChange it is called on
130 // and asserts its internal validity, or lack thereof.
131 func (c ClientChange) Validate() []error {
134 errors = append(errors, ErrEmptyChange)
137 if c.Name != nil && len(*c.Name) < 2 {
138 errors = append(errors, ErrClientNameTooShort)
140 if c.Name != nil && len(*c.Name) > 32 {
141 errors = append(errors, ErrClientNameTooLong)
143 if c.Logo != nil && *c.Logo != "" {
144 if len(*c.Logo) > 1024 {
145 errors = append(errors, ErrClientLogoTooLong)
147 u, err := url.Parse(*c.Logo)
148 if err != nil || !u.IsAbs() {
149 errors = append(errors, ErrClientLogoNotURL)
152 if c.Website != nil && *c.Website != "" {
153 if len(*c.Website) > 140 {
154 errors = append(errors, ErrClientWebsiteTooLong)
156 u, err := url.Parse(*c.Website)
157 if err != nil || !u.IsAbs() {
158 errors = append(errors, ErrClientWebsiteNotURL)
164 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) {
165 enc := json.NewEncoder(w)
166 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
168 clientIDStr = r.PostFormValue("client_id")
170 if clientIDStr == "" {
171 w.WriteHeader(http.StatusUnauthorized)
173 w.Header().Set("WWW-Authenticate", "Basic")
175 renderJSONError(enc, "invalid_client")
176 return nil, "", false
178 if !allowPublic && !fromAuthHeader {
179 w.WriteHeader(http.StatusBadRequest)
180 renderJSONError(enc, "unauthorized_client")
181 return nil, "", false
183 clientID, err := uuid.Parse(clientIDStr)
185 log.Println("Error decoding client ID:", err)
186 w.WriteHeader(http.StatusUnauthorized)
188 w.Header().Set("WWW-Authenticate", "Basic")
190 renderJSONError(enc, "invalid_client")
191 return nil, "", false
193 return clientID, clientSecret, true
196 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
197 enc := json.NewEncoder(w)
198 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic)
202 _, _, fromAuthHeader := r.BasicAuth()
203 client, err := context.GetClient(clientID)
204 if err == ErrClientNotFound {
205 w.WriteHeader(http.StatusUnauthorized)
207 w.Header().Set("WWW-Authenticate", "Basic")
209 renderJSONError(enc, "invalid_client")
211 } else if err != nil {
212 w.WriteHeader(http.StatusInternalServerError)
213 renderJSONError(enc, "server_error")
216 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
217 w.WriteHeader(http.StatusUnauthorized)
219 w.Header().Set("WWW-Authenticate", "Basic")
221 renderJSONError(enc, "invalid_client")
224 return clientID, true
227 // Endpoint represents a single URI that a Client
228 // controls. Users will be redirected to these URIs
229 // following successful authorization grants and
230 // exchanges for access tokens.
231 type Endpoint struct {
232 ID uuid.ID `json:"id,omitempty"`
233 ClientID uuid.ID `json:"client_id,omitempty"`
234 URI string `json:"uri,omitempty"`
235 NormalizedURI string `json:"-"`
236 Added time.Time `json:"added,omitempty"`
239 func normalizeURIString(in string) (string, error) {
240 n, err := purell.NormalizeURLString(in, normalizeFlags)
243 return in, ErrEndpointURINotURL
248 func normalizeURI(in *url.URL) string {
249 return purell.NormalizeURL(in, normalizeFlags)
252 type sortedEndpoints []Endpoint
254 func (s sortedEndpoints) Len() int {
258 func (s sortedEndpoints) Less(i, j int) bool {
259 return s[i].Added.Before(s[j].Added)
262 func (s sortedEndpoints) Swap(i, j int) {
263 s[i], s[j] = s[j], s[i]
266 type clientStore interface {
267 getClient(id uuid.ID) (Client, error)
268 saveClient(client Client) error
269 updateClient(id uuid.ID, change ClientChange) error
270 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
272 addEndpoints(endpoint []Endpoint) error
273 removeEndpoint(client, endpoint uuid.ID) error
274 getEndpoint(client, endpoint uuid.ID) (Endpoint, error)
275 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
276 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
277 countEndpoints(client uuid.ID) (int64, error)
280 func (m *memstore) getClient(id uuid.ID) (Client, error) {
282 defer m.clientLock.RUnlock()
283 c, ok := m.clients[id.String()]
284 if !ok || c.Deleted {
285 return Client{}, ErrClientNotFound
290 func (m *memstore) saveClient(client Client) error {
292 defer m.clientLock.Unlock()
293 if _, ok := m.clients[client.ID.String()]; ok {
294 return ErrClientAlreadyExists
296 m.clients[client.ID.String()] = client
297 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
301 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
303 defer m.clientLock.Unlock()
304 c, ok := m.clients[id.String()]
306 return ErrClientNotFound
308 c.ApplyChange(change)
309 m.clients[id.String()] = c
313 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
314 ids := m.lookupClientsByProfileID(ownerID.String())
315 if len(ids) > num+offset {
316 ids = ids[offset : num+offset]
317 } else if len(ids) > offset {
320 return []Client{}, nil
322 clients := []Client{}
323 for _, id := range ids {
324 client, err := m.getClient(id)
326 if err == ErrClientNotFound {
329 return []Client{}, err
331 clients = append(clients, client)
336 func (m *memstore) addEndpoints(endpoints []Endpoint) error {
337 m.endpointLock.Lock()
338 defer m.endpointLock.Unlock()
339 clients := map[string][]Endpoint{}
340 for _, endpoint := range endpoints {
341 clients[endpoint.ClientID.String()] = append(clients[endpoint.ClientID.String()], endpoint)
343 for client, e := range clients {
344 m.endpoints[client] = append(m.endpoints[client], e...)
349 func (m *memstore) getEndpoint(client, endpoint uuid.ID) (Endpoint, error) {
350 m.endpointLock.Lock()
351 defer m.endpointLock.Unlock()
352 for _, item := range m.endpoints[client.String()] {
353 if item.ID.Equal(endpoint) {
357 return Endpoint{}, ErrEndpointNotFound
360 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
361 m.endpointLock.Lock()
362 defer m.endpointLock.Unlock()
364 for p, item := range m.endpoints[client.String()] {
365 if item.ID.Equal(endpoint) {
371 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
376 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
377 m.endpointLock.RLock()
378 defer m.endpointLock.RUnlock()
379 for _, candidate := range m.endpoints[client.String()] {
380 if endpoint == candidate.NormalizedURI {
387 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
388 m.endpointLock.RLock()
389 defer m.endpointLock.RUnlock()
390 return m.endpoints[client.String()], nil
393 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
394 m.endpointLock.RLock()
395 defer m.endpointLock.RUnlock()
396 return int64(len(m.endpoints[client.String()])), nil
399 type newClientReq struct {
400 Name string `json:"name"`
401 Logo string `json:"logo"`
402 Website string `json:"website"`
403 Type string `json:"type"`
404 Endpoints []string `json:"endpoints"`
407 func RegisterClientHandlers(r *mux.Router, context Context) {
408 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
409 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET")
410 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET")
411 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH")
412 r.Handle("/clients/{id}", wrap(context, RemoveClientHandler)).Methods("DELETE")
413 r.Handle("/clients/{id}/endpoints", wrap(context, AddEndpointsHandler)).Methods("POST")
414 r.Handle("/clients/{client_id}/endpoints/{id}", wrap(context, RemoveEndpointHandler)).Methods("DELETE")
415 r.Handle("/clients/{id}/endpoints", wrap(context, ListEndpointsHandler)).Methods("GET")
418 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
419 errors := []requestError{}
420 username, password, ok := r.BasicAuth()
422 errors = append(errors, requestError{Slug: requestErrAccessDenied})
423 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
426 profile, err := authenticate(username, password, c)
428 if isAuthError(err) {
429 errors = append(errors, requestError{Slug: requestErrAccessDenied})
430 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
432 log.Printf("Error authenticating: %#+v\n", err)
433 errors = append(errors, requestError{Slug: requestErrActOfGod})
434 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
439 decoder := json.NewDecoder(r.Body)
440 err = decoder.Decode(&req)
442 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
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"})
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"})
458 encode(w, r, http.StatusBadRequest, response{Errors: errors})
466 Website: req.Website,
469 if client.Type == clientTypeConfidential {
470 secret := make([]byte, 32)
471 _, err = rand.Read(secret)
473 log.Printf("Error generating secret: %#+v\n", err)
474 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
477 client.Secret = hex.EncodeToString(secret)
479 err = c.SaveClient(client)
481 if err == ErrClientAlreadyExists {
482 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
483 encode(w, r, http.StatusBadRequest, response{Errors: errors})
486 log.Printf("Error saving client: %#+v\n", err)
487 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
490 endpoints := []Endpoint{}
491 for pos, u := range req.Endpoints {
492 uri, err := url.Parse(u)
494 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
498 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
501 endpoint := Endpoint{
507 endpoints = append(endpoints, endpoint)
509 err = c.AddEndpoints(endpoints)
511 log.Printf("Error adding endpoints: %#+v\n", err)
512 errors = append(errors, requestError{Slug: requestErrActOfGod})
513 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
517 Clients: []Client{client},
518 Endpoints: endpoints,
521 encode(w, r, http.StatusCreated, resp)
524 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
525 errors := []requestError{}
527 if vars["id"] == "" {
528 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
529 encode(w, r, http.StatusBadRequest, response{Errors: errors})
532 id, err := uuid.Parse(vars["id"])
534 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
535 encode(w, r, http.StatusBadRequest, response{Errors: errors})
538 client, err := c.GetClient(id)
540 if err == ErrClientNotFound {
541 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
542 encode(w, r, http.StatusNotFound, response{Errors: errors})
545 errors = append(errors, requestError{Slug: requestErrActOfGod})
546 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
549 username, password, ok := r.BasicAuth()
553 profile, err := authenticate(username, password, c)
555 if isAuthError(err) {
556 errors = append(errors, requestError{Slug: requestErrAccessDenied})
557 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
559 errors = append(errors, requestError{Slug: requestErrActOfGod})
560 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
564 if !client.OwnerID.Equal(profile.ID) {
569 Clients: []Client{client},
572 encode(w, r, http.StatusOK, resp)
575 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
576 errors := []requestError{}
578 // BUG(paddy): If ids are provided in query params, retrieve only those clients
579 num := defaultClientResponseSize
581 ownerIDStr := r.URL.Query().Get("owner_id")
582 numStr := r.URL.Query().Get("num")
583 offsetStr := r.URL.Query().Get("offset")
585 num, err = strconv.Atoi(numStr)
587 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
589 if num > maxClientResponseSize {
590 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
594 offset, err = strconv.Atoi(offsetStr)
596 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
599 if ownerIDStr == "" {
600 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
603 encode(w, r, http.StatusBadRequest, response{Errors: errors})
606 ownerID, err := uuid.Parse(ownerIDStr)
608 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
609 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
612 clients, err := c.ListClientsByOwner(ownerID, num, offset)
614 errors = append(errors, requestError{Slug: requestErrActOfGod})
615 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
618 username, password, ok := r.BasicAuth()
620 for pos, client := range clients {
622 clients[pos] = client
625 profile, err := authenticate(username, password, c)
627 if isAuthError(err) {
628 errors = append(errors, requestError{Slug: requestErrAccessDenied})
629 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
631 errors = append(errors, requestError{Slug: requestErrActOfGod})
632 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
636 for pos, client := range clients {
637 if !client.OwnerID.Equal(profile.ID) {
639 clients[pos] = client
647 encode(w, r, http.StatusOK, resp)
650 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
651 errors := []requestError{}
653 if _, ok := vars["id"]; !ok {
654 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
655 encode(w, r, http.StatusBadRequest, response{Errors: errors})
658 id, err := uuid.Parse(vars["id"])
660 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
662 username, password, ok := r.BasicAuth()
664 errors = append(errors, requestError{Slug: requestErrAccessDenied})
665 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
668 profile, err := authenticate(username, password, c)
670 if isAuthError(err) {
671 errors = append(errors, requestError{Slug: requestErrAccessDenied})
672 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
674 errors = append(errors, requestError{Slug: requestErrActOfGod})
675 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
679 var change ClientChange
680 err = decode(r, &change)
682 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
683 encode(w, r, http.StatusBadRequest, response{Errors: errors})
686 errs := change.Validate()
687 for _, err := range errs {
690 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"})
691 case ErrClientNameTooShort:
692 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
693 case ErrClientNameTooLong:
694 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
695 case ErrClientLogoTooLong:
696 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"})
697 case ErrClientLogoNotURL:
698 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"})
699 case ErrClientWebsiteTooLong:
700 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"})
701 case ErrClientWebsiteNotURL:
702 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"})
704 log.Println("Unrecognised error from client change validation:", err)
708 encode(w, r, http.StatusBadRequest, response{Errors: errors})
711 client, err := c.GetClient(id)
712 if err == ErrClientNotFound {
713 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
714 encode(w, r, http.StatusNotFound, response{Errors: errors})
716 } else if err != nil {
717 log.Println("Error retrieving client:", err)
718 errors = append(errors, requestError{Slug: requestErrActOfGod})
719 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
722 if !client.OwnerID.Equal(profile.ID) {
723 errors = append(errors, requestError{Slug: requestErrAccessDenied})
724 encode(w, r, http.StatusForbidden, response{Errors: errors})
727 if change.Secret != nil && client.Type == clientTypeConfidential {
728 secret := make([]byte, 32)
729 _, err = rand.Read(secret)
731 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
734 newSecret := hex.EncodeToString(secret)
735 change.Secret = &newSecret
737 err = c.UpdateClient(id, change)
739 log.Println("Error updating client:", err)
740 errors = append(errors, requestError{Slug: requestErrActOfGod})
741 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
744 client.ApplyChange(change)
745 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
749 func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
750 errors := []requestError{}
752 if _, ok := vars["id"]; !ok {
753 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
754 encode(w, r, http.StatusNotFound, response{Errors: errors})
757 id, err := uuid.Parse(vars["id"])
759 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
761 username, password, ok := r.BasicAuth()
763 errors = append(errors, requestError{Slug: requestErrAccessDenied})
764 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
767 profile, err := authenticate(username, password, c)
769 if isAuthError(err) {
770 errors = append(errors, requestError{Slug: requestErrAccessDenied})
771 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
773 errors = append(errors, requestError{Slug: requestErrActOfGod})
774 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
778 client, err := c.GetClient(id)
780 if err == ErrClientNotFound {
781 errors = append(errors, requestError{Slug: requestErrNotFound})
782 encode(w, r, http.StatusNotFound, response{Errors: errors})
785 log.Println("Error retrieving client:", err)
786 errors = append(errors, requestError{Slug: requestErrActOfGod})
787 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
790 if !client.OwnerID.Equal(profile.ID) {
791 errors = append(errors, requestError{Slug: requestErrAccessDenied})
792 encode(w, r, http.StatusForbidden, response{Errors: errors})
796 change := ClientChange{Deleted: &deleted}
797 err = c.UpdateClient(id, change)
799 if err == ErrClientNotFound {
800 errors = append(errors, requestError{Slug: requestErrNotFound})
801 encode(w, r, http.StatusNotFound, response{Errors: errors})
804 log.Println("Error deleting client:", err)
805 errors = append(errors, requestError{Slug: requestErrActOfGod})
806 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
809 // BUG(paddy): Client needs to clean up after itself, invalidating tokens, deleting unused grants, deleting endpoints
810 encode(w, r, http.StatusOK, response{Errors: errors})
814 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
815 type addEndpointReq struct {
816 Endpoints []string `json:"endpoints"`
818 errors := []requestError{}
820 if vars["id"] == "" {
821 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
822 encode(w, r, http.StatusBadRequest, response{Errors: errors})
825 id, err := uuid.Parse(vars["id"])
827 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
828 encode(w, r, http.StatusBadRequest, response{Errors: errors})
831 username, password, ok := r.BasicAuth()
833 errors = append(errors, requestError{Slug: requestErrAccessDenied})
834 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
837 profile, err := authenticate(username, password, c)
839 if isAuthError(err) {
840 errors = append(errors, requestError{Slug: requestErrAccessDenied})
841 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
843 errors = append(errors, requestError{Slug: requestErrActOfGod})
844 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
848 client, err := c.GetClient(id)
850 if err == ErrClientNotFound {
851 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
852 encode(w, r, http.StatusBadRequest, response{Errors: errors})
855 errors = append(errors, requestError{Slug: requestErrActOfGod})
856 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
859 if !client.OwnerID.Equal(profile.ID) {
860 errors = append(errors, requestError{Slug: requestErrAccessDenied})
861 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
864 var req addEndpointReq
865 decoder := json.NewDecoder(r.Body)
866 err = decoder.Decode(&req)
868 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
871 if len(req.Endpoints) < 1 {
872 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"})
873 encode(w, r, http.StatusBadRequest, response{Errors: errors})
876 endpoints := []Endpoint{}
877 for pos, u := range req.Endpoints {
878 if parsed, err := url.Parse(u); err != nil {
879 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
881 } else if !parsed.IsAbs() {
882 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
891 endpoints = append(endpoints, e)
894 encode(w, r, http.StatusBadRequest, response{Errors: errors})
897 err = c.AddEndpoints(endpoints)
899 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
904 Endpoints: endpoints,
906 encode(w, r, http.StatusCreated, resp)
909 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
910 errors := []requestError{}
912 clientID, err := uuid.Parse(vars["id"])
914 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
915 encode(w, r, http.StatusBadRequest, response{Errors: errors})
918 num := defaultEndpointResponseSize
920 numStr := r.URL.Query().Get("num")
921 offsetStr := r.URL.Query().Get("offset")
923 num, err = strconv.Atoi(numStr)
925 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
927 if num > maxEndpointResponseSize {
928 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
932 offset, err = strconv.Atoi(offsetStr)
934 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
938 encode(w, r, http.StatusBadRequest, response{Errors: errors})
941 endpoints, err := c.ListEndpoints(clientID, num, offset)
943 errors = append(errors, requestError{Slug: requestErrActOfGod})
944 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
948 Endpoints: endpoints,
951 encode(w, r, http.StatusOK, resp)
954 func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) {
955 errors := []requestError{}
957 if vars["client_id"] == "" {
958 errors = append(errors, requestError{Slug: requestErrMissing, Param: "client_id"})
959 encode(w, r, http.StatusBadRequest, response{Errors: errors})
962 clientID, err := uuid.Parse(vars["client_id"])
964 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
965 encode(w, r, http.StatusBadRequest, response{Errors: errors})
968 if vars["id"] == "" {
969 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
970 encode(w, r, http.StatusBadRequest, response{Errors: errors})
973 id, err := uuid.Parse(vars["id"])
975 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
976 encode(w, r, http.StatusBadRequest, response{Errors: errors})
979 username, password, ok := r.BasicAuth()
981 errors = append(errors, requestError{Slug: requestErrAccessDenied})
982 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
985 profile, err := authenticate(username, password, c)
987 if isAuthError(err) {
988 errors = append(errors, requestError{Slug: requestErrAccessDenied})
989 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
991 errors = append(errors, requestError{Slug: requestErrActOfGod})
992 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
996 client, err := c.GetClient(clientID)
998 if err == ErrClientNotFound {
999 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "client_id"})
1000 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1003 errors = append(errors, requestError{Slug: requestErrActOfGod})
1004 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1007 if !client.OwnerID.Equal(profile.ID) {
1008 errors = append(errors, requestError{Slug: requestErrAccessDenied})
1009 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
1012 endpoint, err := c.GetEndpoint(clientID, id)
1014 if err == ErrEndpointNotFound {
1015 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
1016 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1019 errors = append(errors, requestError{Slug: requestErrActOfGod})
1020 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1023 err = c.RemoveEndpoint(clientID, id)
1025 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
1030 Endpoints: []Endpoint{endpoint},
1032 encode(w, r, http.StatusCreated, resp)
1035 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
1036 scopes = strings.Split(r.PostFormValue("scope"), " ")
1041 func clientCredentialsAuditString(r *http.Request) string {
1042 return "client_credentials"