Break out scopes and events.
This repo has gotten unwieldy, and there are portions of it that need to be
imported by a large number of other packages.
For example, scopes will be used in almost every API we write. Rather than
importing the entirety of this codebase into every API we write, I've opted to
move the scope logic out into a scopes package, with a subpackage for the
defined types, which is all most projects actually want to import.
We also define some event type constants, and importing those shouldn't require
a project to import all our dependencies, either. So I made an events subpackage
that just holds those constants.
This package has become a little bit of a red-headed stepchild and is do for a
refactor, but I'm trying to put that off as long as I can.
The refactoring of our scopes stuff has left a bug wherein a token can be
granted for scopes that don't exist. I'm going to need to revisit that, and also
how to limit scopes to only be granted to the users that should be able to
request them. But that's a battle for another day.
15 "github.com/PuerkitoBio/purell"
16 "github.com/gorilla/mux"
18 "code.secondbit.org/scopes.hg/types"
19 "code.secondbit.org/uuid.hg"
23 RegisterGrantType("client_credentials", GrantType{
24 Validate: clientCredentialsValidate,
27 ReturnToken: RenderJSONToken,
29 AuditString: clientCredentialsAuditString,
34 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first.
35 ErrNoClientStore = errors.New("no clientStore was specified for the Context")
36 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore.
37 ErrClientNotFound = errors.New("client not found in clientStore")
38 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with
39 // the same ID already exists in the clientStore.
40 ErrClientAlreadyExists = errors.New("client already exists in clientStore")
41 // ErrEndpointNotFound is returned when an Endpoint is requested but not found in a clientSTore.
42 ErrEndpointNotFound = errors.New("endpoint not found in clientStore")
43 // ErrEndpointAlreadyExists is returned when an Endpoint is added to a clientStore, but another Endpoint
44 // with the same ID already exists in the clientStore.
45 ErrEndpointAlreadyExists = errors.New("endpoint already exists in clientStore")
47 // ErrEmptyChange is returned when a Change has all its properties set to nil.
48 ErrEmptyChange = errors.New("change must have at least one property set")
49 // ErrClientNameTooShort is returned when a Client's Name property is too short.
50 ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
51 // ErrClientNameTooLong is returned when a Client's Name property is too long.
52 ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
53 // ErrClientLogoTooLong is returned when a Client's Logo property is too long.
54 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
55 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL.
56 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
57 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long.
58 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
59 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL.
60 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
61 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL.
62 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL")
66 clientTypePublic = "public"
67 clientTypeConfidential = "confidential"
70 defaultClientResponseSize = 20
71 maxClientResponseSize = 50
72 defaultEndpointResponseSize = 20
73 maxEndpointResponseSize = 50
75 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery
78 // Client represents a client that grants access
79 // to the auth server, exchanging grants for tokens,
80 // and tokens for access.
82 ID uuid.ID `json:"id,omitempty"`
83 Secret string `json:"secret,omitempty"`
84 OwnerID uuid.ID `json:"owner_id,omitempty"`
85 Name string `json:"name,omitempty"`
86 Logo string `json:"logo,omitempty"`
87 Website string `json:"website,omitempty"`
88 Type string `json:"type,omitempty"`
89 Deleted bool `json:"deleted,omitempty"`
92 // ApplyChange applies the properties of the passed
93 // ClientChange to the Client object it is called on.
94 func (c *Client) ApplyChange(change ClientChange) {
95 if change.Secret != nil {
96 c.Secret = *change.Secret
98 if change.OwnerID != nil {
99 c.OwnerID = change.OwnerID
101 if change.Name != nil {
102 c.Name = *change.Name
104 if change.Logo != nil {
105 c.Logo = *change.Logo
107 if change.Website != nil {
108 c.Website = *change.Website
110 if change.Deleted != nil {
111 c.Deleted = *change.Deleted
115 // ClientChange represents a bundle of options for
116 // updating a Client's mutable data.
117 type ClientChange struct {
126 func (c ClientChange) Empty() bool {
127 return c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil && c.Deleted == nil
130 // Validate checks the ClientChange it is called on
131 // and asserts its internal validity, or lack thereof.
132 func (c ClientChange) Validate() []error {
135 errors = append(errors, ErrEmptyChange)
138 if c.Name != nil && len(*c.Name) < 2 {
139 errors = append(errors, ErrClientNameTooShort)
141 if c.Name != nil && len(*c.Name) > 32 {
142 errors = append(errors, ErrClientNameTooLong)
144 if c.Logo != nil && *c.Logo != "" {
145 if len(*c.Logo) > 1024 {
146 errors = append(errors, ErrClientLogoTooLong)
148 u, err := url.Parse(*c.Logo)
149 if err != nil || !u.IsAbs() {
150 errors = append(errors, ErrClientLogoNotURL)
153 if c.Website != nil && *c.Website != "" {
154 if len(*c.Website) > 140 {
155 errors = append(errors, ErrClientWebsiteTooLong)
157 u, err := url.Parse(*c.Website)
158 if err != nil || !u.IsAbs() {
159 errors = append(errors, ErrClientWebsiteNotURL)
165 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) {
166 enc := json.NewEncoder(w)
167 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
169 clientIDStr = r.PostFormValue("client_id")
171 if clientIDStr == "" {
172 w.WriteHeader(http.StatusUnauthorized)
174 w.Header().Set("WWW-Authenticate", "Basic")
176 renderJSONError(enc, "invalid_client")
177 return nil, "", false
179 if !allowPublic && !fromAuthHeader {
180 w.WriteHeader(http.StatusBadRequest)
181 renderJSONError(enc, "unauthorized_client")
182 return nil, "", false
184 clientID, err := uuid.Parse(clientIDStr)
186 log.Println("Error decoding client ID:", err)
187 w.WriteHeader(http.StatusUnauthorized)
189 w.Header().Set("WWW-Authenticate", "Basic")
191 renderJSONError(enc, "invalid_client")
192 return nil, "", false
194 return clientID, clientSecret, true
197 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
198 enc := json.NewEncoder(w)
199 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic)
203 _, _, fromAuthHeader := r.BasicAuth()
204 client, err := context.GetClient(clientID)
205 if err == ErrClientNotFound {
206 w.WriteHeader(http.StatusUnauthorized)
208 w.Header().Set("WWW-Authenticate", "Basic")
210 renderJSONError(enc, "invalid_client")
212 } else if err != nil {
213 w.WriteHeader(http.StatusInternalServerError)
214 renderJSONError(enc, "server_error")
217 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
218 w.WriteHeader(http.StatusUnauthorized)
220 w.Header().Set("WWW-Authenticate", "Basic")
222 renderJSONError(enc, "invalid_client")
225 return clientID, true
228 // Endpoint represents a single URI that a Client
229 // controls. Users will be redirected to these URIs
230 // following successful authorization grants and
231 // exchanges for access tokens.
232 type Endpoint struct {
233 ID uuid.ID `json:"id,omitempty"`
234 ClientID uuid.ID `json:"client_id,omitempty"`
235 URI string `json:"uri,omitempty"`
236 NormalizedURI string `json:"-"`
237 Added time.Time `json:"added,omitempty"`
240 func normalizeURIString(in string) (string, error) {
241 n, err := purell.NormalizeURLString(in, normalizeFlags)
244 return in, ErrEndpointURINotURL
249 func normalizeURI(in *url.URL) string {
250 return purell.NormalizeURL(in, normalizeFlags)
253 type sortedEndpoints []Endpoint
255 func (s sortedEndpoints) Len() int {
259 func (s sortedEndpoints) Less(i, j int) bool {
260 return s[i].Added.Before(s[j].Added)
263 func (s sortedEndpoints) Swap(i, j int) {
264 s[i], s[j] = s[j], s[i]
267 type clientStore interface {
268 getClient(id uuid.ID) (Client, error)
269 saveClient(client Client) error
270 updateClient(id uuid.ID, change ClientChange) error
271 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
272 deleteClientsByOwner(ownerID uuid.ID) error
274 addEndpoints(endpoint []Endpoint) error
275 removeEndpoint(client, endpoint uuid.ID) error
276 getEndpoint(client, endpoint uuid.ID) (Endpoint, error)
277 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
278 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
279 removeEndpointsByClientID(client uuid.ID) error
280 countEndpoints(client uuid.ID) (int64, error)
283 func (m *memstore) getClient(id uuid.ID) (Client, error) {
285 defer m.clientLock.RUnlock()
286 c, ok := m.clients[id.String()]
287 if !ok || c.Deleted {
288 return Client{}, ErrClientNotFound
293 func (m *memstore) saveClient(client Client) error {
295 defer m.clientLock.Unlock()
296 if _, ok := m.clients[client.ID.String()]; ok {
297 return ErrClientAlreadyExists
299 m.clients[client.ID.String()] = client
300 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
304 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
306 defer m.clientLock.Unlock()
307 c, ok := m.clients[id.String()]
309 return ErrClientNotFound
311 c.ApplyChange(change)
312 m.clients[id.String()] = c
316 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
317 ids := m.lookupClientsByProfileID(ownerID.String())
318 if len(ids) > num+offset && num > 0 {
319 ids = ids[offset : num+offset]
320 } else if len(ids) > offset {
323 return []Client{}, nil
325 clients := []Client{}
326 for _, id := range ids {
327 client, err := m.getClient(id)
329 if err == ErrClientNotFound {
332 return []Client{}, err
334 clients = append(clients, client)
339 func (m *memstore) deleteClientsByOwner(ownerID uuid.ID) error {
340 ids := m.lookupClientsByProfileID(ownerID.String())
342 defer m.clientLock.RUnlock()
343 for _, id := range ids {
344 client, ok := m.clients[id.String()]
348 client.Deleted = true
349 m.clients[id.String()] = client
354 func (m *memstore) addEndpoints(endpoints []Endpoint) error {
355 m.endpointLock.Lock()
356 defer m.endpointLock.Unlock()
357 clients := map[string][]Endpoint{}
358 for _, endpoint := range endpoints {
359 clients[endpoint.ClientID.String()] = append(clients[endpoint.ClientID.String()], endpoint)
361 for client, e := range clients {
362 m.endpoints[client] = append(m.endpoints[client], e...)
367 func (m *memstore) getEndpoint(client, endpoint uuid.ID) (Endpoint, error) {
368 m.endpointLock.Lock()
369 defer m.endpointLock.Unlock()
370 for _, item := range m.endpoints[client.String()] {
371 if item.ID.Equal(endpoint) {
375 return Endpoint{}, ErrEndpointNotFound
378 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
379 m.endpointLock.Lock()
380 defer m.endpointLock.Unlock()
382 for p, item := range m.endpoints[client.String()] {
383 if item.ID.Equal(endpoint) {
389 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
394 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
395 m.endpointLock.RLock()
396 defer m.endpointLock.RUnlock()
397 for _, candidate := range m.endpoints[client.String()] {
398 if endpoint == candidate.NormalizedURI {
405 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
406 m.endpointLock.RLock()
407 defer m.endpointLock.RUnlock()
408 return m.endpoints[client.String()], nil
411 func (m *memstore) removeEndpointsByClientID(client uuid.ID) error {
412 m.endpointLock.Lock()
413 defer m.endpointLock.Unlock()
414 delete(m.endpoints, client.String())
418 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
419 m.endpointLock.RLock()
420 defer m.endpointLock.RUnlock()
421 return int64(len(m.endpoints[client.String()])), nil
424 func cleanUpAfterClientDeletion(client uuid.ID, context Context) {
425 err := context.RemoveEndpointsByClientID(client)
427 log.Printf("Error removing endpoints from client %s: %+v\n", client, err)
429 err = context.DeleteAuthorizationCodesByClientID(client)
431 log.Printf("Error removing auth codes belonging to client %s: %+v\n", client, err)
433 err = context.RevokeTokensByClientID(client)
435 log.Printf("Error revoking tokens belonging to client %s: %+v\n", client, err)
439 type newClientReq struct {
440 Name string `json:"name"`
441 Logo string `json:"logo"`
442 Website string `json:"website"`
443 Type string `json:"type"`
444 Endpoints []string `json:"endpoints"`
447 func RegisterClientHandlers(r *mux.Router, context Context) {
448 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
449 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET")
450 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET")
451 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH")
452 r.Handle("/clients/{id}", wrap(context, RemoveClientHandler)).Methods("DELETE")
453 r.Handle("/clients/{id}/endpoints", wrap(context, AddEndpointsHandler)).Methods("POST")
454 r.Handle("/clients/{client_id}/endpoints/{id}", wrap(context, RemoveEndpointHandler)).Methods("DELETE")
455 r.Handle("/clients/{id}/endpoints", wrap(context, ListEndpointsHandler)).Methods("GET")
458 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
459 errors := []RequestError{}
460 username, password, ok := r.BasicAuth()
462 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
463 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
466 profile, err := authenticate(username, password, c)
468 if isAuthError(err) {
469 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
470 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
472 log.Printf("Error authenticating: %#+v\n", err)
473 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
474 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
479 decoder := json.NewDecoder(r.Body)
480 err = decoder.Decode(&req)
482 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
486 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/type"})
487 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
488 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/type"})
491 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/name"})
492 } else if len(req.Name) < minClientNameLen {
493 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/name"})
494 } else if len(req.Name) > maxClientNameLen {
495 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/name"})
498 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
506 Website: req.Website,
509 if client.Type == clientTypeConfidential {
510 secret := make([]byte, 32)
511 _, err = rand.Read(secret)
513 log.Printf("Error generating secret: %#+v\n", err)
514 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
517 client.Secret = hex.EncodeToString(secret)
519 err = c.SaveClient(client)
521 if err == ErrClientAlreadyExists {
522 errors = append(errors, RequestError{Slug: RequestErrConflict, Field: "/id"})
523 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
526 log.Printf("Error saving client: %#+v\n", err)
527 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
530 endpoints := []Endpoint{}
531 for pos, u := range req.Endpoints {
532 uri, err := url.Parse(u)
534 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
538 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
541 endpoint := Endpoint{
547 endpoints = append(endpoints, endpoint)
549 err = c.AddEndpoints(endpoints)
551 log.Printf("Error adding endpoints: %#+v\n", err)
552 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
553 encode(w, r, http.StatusInternalServerError, Response{Errors: errors, Clients: []Client{client}})
557 Clients: []Client{client},
558 Endpoints: endpoints,
561 encode(w, r, http.StatusCreated, resp)
564 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
565 errors := []RequestError{}
567 if vars["id"] == "" {
568 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
569 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
572 id, err := uuid.Parse(vars["id"])
574 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
575 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
578 client, err := c.GetClient(id)
580 if err == ErrClientNotFound {
581 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
582 encode(w, r, http.StatusNotFound, Response{Errors: errors})
585 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
586 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
589 username, password, ok := r.BasicAuth()
593 profile, err := authenticate(username, password, c)
595 if isAuthError(err) {
596 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
597 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
599 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
600 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
604 if !client.OwnerID.Equal(profile.ID) {
609 Clients: []Client{client},
612 encode(w, r, http.StatusOK, resp)
615 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
616 errors := []RequestError{}
618 // BUG(paddy): If ids are provided in query params, retrieve only those clients
619 num := defaultClientResponseSize
621 ownerIDStr := r.URL.Query().Get("owner_id")
622 numStr := r.URL.Query().Get("num")
623 offsetStr := r.URL.Query().Get("offset")
625 num, err = strconv.Atoi(numStr)
627 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "num"})
629 if num > maxClientResponseSize {
630 errors = append(errors, RequestError{Slug: RequestErrOverflow, Param: "num"})
633 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Param: "num"})
637 offset, err = strconv.Atoi(offsetStr)
639 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "offset"})
642 if ownerIDStr == "" {
643 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "owner_id"})
646 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
649 ownerID, err := uuid.Parse(ownerIDStr)
651 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "owner_id"})
652 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
655 clients, err := c.ListClientsByOwner(ownerID, num, offset)
657 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
658 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
661 username, password, ok := r.BasicAuth()
663 for pos, client := range clients {
665 clients[pos] = client
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 for pos, client := range clients {
680 if !client.OwnerID.Equal(profile.ID) {
682 clients[pos] = client
690 encode(w, r, http.StatusOK, resp)
693 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
694 errors := []RequestError{}
696 if _, ok := vars["id"]; !ok {
697 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
698 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
701 id, err := uuid.Parse(vars["id"])
703 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
705 username, password, ok := r.BasicAuth()
707 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
708 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
711 profile, err := authenticate(username, password, c)
713 if isAuthError(err) {
714 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
715 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
717 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
718 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
722 var change ClientChange
723 err = decode(r, &change)
725 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/"})
726 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
729 errs := change.Validate()
730 for _, err := range errs {
733 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
734 case ErrClientNameTooShort:
735 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/name"})
736 case ErrClientNameTooLong:
737 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/name"})
738 case ErrClientLogoTooLong:
739 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/logo"})
740 case ErrClientLogoNotURL:
741 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/logo"})
742 case ErrClientWebsiteTooLong:
743 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/website"})
744 case ErrClientWebsiteNotURL:
745 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/website"})
747 log.Println("Unrecognised error from client change validation:", err)
751 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
754 client, err := c.GetClient(id)
755 if err == ErrClientNotFound {
756 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
757 encode(w, r, http.StatusNotFound, Response{Errors: errors})
759 } else if err != nil {
760 log.Println("Error retrieving client:", err)
761 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
762 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
765 if !client.OwnerID.Equal(profile.ID) {
766 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
767 encode(w, r, http.StatusForbidden, Response{Errors: errors})
770 if change.Secret != nil && client.Type == clientTypeConfidential {
771 secret := make([]byte, 32)
772 _, err = rand.Read(secret)
774 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
777 newSecret := hex.EncodeToString(secret)
778 change.Secret = &newSecret
780 err = c.UpdateClient(id, change)
782 log.Println("Error updating client:", err)
783 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
784 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
787 client.ApplyChange(change)
788 encode(w, r, http.StatusOK, Response{Clients: []Client{client}, Errors: errors})
792 func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
793 errors := []RequestError{}
795 if _, ok := vars["id"]; !ok {
796 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
797 encode(w, r, http.StatusNotFound, Response{Errors: errors})
800 id, err := uuid.Parse(vars["id"])
802 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
804 username, password, ok := r.BasicAuth()
806 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
807 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
810 profile, err := authenticate(username, password, c)
812 if isAuthError(err) {
813 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
814 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
816 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
817 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
821 client, err := c.GetClient(id)
823 if err == ErrClientNotFound {
824 errors = append(errors, RequestError{Slug: RequestErrNotFound})
825 encode(w, r, http.StatusNotFound, Response{Errors: errors})
828 log.Println("Error retrieving client:", err)
829 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
830 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
833 if !client.OwnerID.Equal(profile.ID) {
834 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
835 encode(w, r, http.StatusForbidden, Response{Errors: errors})
839 change := ClientChange{Deleted: &deleted}
840 err = c.UpdateClient(id, change)
842 if err == ErrClientNotFound {
843 errors = append(errors, RequestError{Slug: RequestErrNotFound})
844 encode(w, r, http.StatusNotFound, Response{Errors: errors})
847 log.Println("Error deleting client:", err)
848 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
849 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
852 encode(w, r, http.StatusOK, Response{Errors: errors})
853 go cleanUpAfterClientDeletion(id, c)
857 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
858 type addEndpointReq struct {
859 Endpoints []string `json:"endpoints"`
861 errors := []RequestError{}
863 if vars["id"] == "" {
864 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
865 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
868 id, err := uuid.Parse(vars["id"])
870 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
871 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
874 username, password, ok := r.BasicAuth()
876 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
877 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
880 profile, err := authenticate(username, password, c)
882 if isAuthError(err) {
883 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
884 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
886 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
887 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
891 client, err := c.GetClient(id)
893 if err == ErrClientNotFound {
894 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
895 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
898 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
899 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
902 if !client.OwnerID.Equal(profile.ID) {
903 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
904 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
907 var req addEndpointReq
908 decoder := json.NewDecoder(r.Body)
909 err = decoder.Decode(&req)
911 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
914 if len(req.Endpoints) < 1 {
915 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/endpoints"})
916 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
919 endpoints := []Endpoint{}
920 for pos, u := range req.Endpoints {
921 if parsed, err := url.Parse(u); err != nil {
922 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
924 } else if !parsed.IsAbs() {
925 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
934 endpoints = append(endpoints, e)
937 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
940 err = c.AddEndpoints(endpoints)
942 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
947 Endpoints: endpoints,
949 encode(w, r, http.StatusCreated, resp)
952 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
953 errors := []RequestError{}
955 clientID, err := uuid.Parse(vars["id"])
957 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "client_id"})
958 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
961 num := defaultEndpointResponseSize
963 numStr := r.URL.Query().Get("num")
964 offsetStr := r.URL.Query().Get("offset")
966 num, err = strconv.Atoi(numStr)
968 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "num"})
970 if num > maxEndpointResponseSize {
971 errors = append(errors, RequestError{Slug: RequestErrOverflow, Param: "num"})
975 offset, err = strconv.Atoi(offsetStr)
977 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "offset"})
981 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
984 endpoints, err := c.ListEndpoints(clientID, num, offset)
986 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
987 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
991 Endpoints: endpoints,
994 encode(w, r, http.StatusOK, resp)
997 func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) {
998 errors := []RequestError{}
1000 if vars["client_id"] == "" {
1001 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "client_id"})
1002 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
1005 clientID, err := uuid.Parse(vars["client_id"])
1007 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "client_id"})
1008 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
1011 if vars["id"] == "" {
1012 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
1013 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
1016 id, err := uuid.Parse(vars["id"])
1018 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
1019 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
1022 username, password, ok := r.BasicAuth()
1024 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
1025 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
1028 profile, err := authenticate(username, password, c)
1030 if isAuthError(err) {
1031 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
1032 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
1034 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
1035 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
1039 client, err := c.GetClient(clientID)
1041 if err == ErrClientNotFound {
1042 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "client_id"})
1043 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
1046 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
1047 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
1050 if !client.OwnerID.Equal(profile.ID) {
1051 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
1052 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
1055 endpoint, err := c.GetEndpoint(clientID, id)
1057 if err == ErrEndpointNotFound {
1058 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
1059 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
1062 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
1063 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
1066 err = c.RemoveEndpoint(clientID, id)
1068 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
1073 Endpoints: []Endpoint{endpoint},
1075 encode(w, r, http.StatusCreated, resp)
1078 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes scopeTypes.Scopes, profileID uuid.ID, valid bool) {
1079 scopes = scopeTypes.StringsToScopes(strings.Split(r.PostFormValue("scope"), " "))
1084 func clientCredentialsAuditString(r *http.Request) string {
1085 return "client_credentials"