Test our Postgres profileStore implementation.
Update all our test cases to use time.Now().Round(time.Millisecond), because Go
uses nanosecond precision on time values, but Postgres silently truncates that
to millisecond precision. This caused our tests to report false failures that
were just silent precision loss, not actual failures.
Set up our authd server to use the Postgres store for profiles and automatically
create a test scope when starting up.
Log errors when creating Clients through the API, instead of just swallowing
them and sending back cryptic act of god errors.
Add a NewPostgres helper that returns a postgres profileStore from a connection
string (passed through pq transparently).
Add an Empty() bool helper to ProfileChange and BulkProfileChange types, so we
can determine if there are any changes we need to act on easily.
Log errors when creating Pofiles through the API, instead of just swalloing them
and sending back cryptic act of god errors.
Remove the ` quotes around field and table names, which are not supported in
Postgres. This required adding a few functions/methods to pan.
Detect situations where a profile was expected and not found, and return
ErrProfileNotFound.
Detect pq errors thrown when the profiles_pkey constraint is violated, and
transform them to the ErrProfileAlreadyExists error.
Detect empty ProfileChange and BulkProfileChange variables and abort the
updateProfile and updateProfiles methods early, before invalid SQL is generated.
Detect pq errors thrown when the logins_pkey constraint is violated, and
transform them to the ErrLoginAlreadyExists error.
Detect when removing a Login and no rows were affected, and return an
ErrLoginNotFound.
Create an sql dir with a postgres_init script that will initialize the schema of
the tables expected in the database.
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")
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")
62 clientTypePublic = "public"
63 clientTypeConfidential = "confidential"
66 defaultClientResponseSize = 20
67 maxClientResponseSize = 50
68 defaultEndpointResponseSize = 20
69 maxEndpointResponseSize = 50
71 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery
74 // Client represents a client that grants access
75 // to the auth server, exchanging grants for tokens,
76 // and tokens for access.
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"`
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
93 if change.OwnerID != nil {
94 c.OwnerID = change.OwnerID
96 if change.Name != nil {
99 if change.Logo != nil {
100 c.Logo = *change.Logo
102 if change.Website != nil {
103 c.Website = *change.Website
107 // ClientChange represents a bundle of options for
108 // updating a Client's mutable data.
109 type ClientChange struct {
117 // Validate checks the ClientChange it is called on
118 // and asserts its internal validity, or lack thereof.
119 func (c ClientChange) Validate() []error {
121 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
122 errors = append(errors, ErrEmptyChange)
125 if c.Name != nil && len(*c.Name) < 2 {
126 errors = append(errors, ErrClientNameTooShort)
128 if c.Name != nil && len(*c.Name) > 32 {
129 errors = append(errors, ErrClientNameTooLong)
131 if c.Logo != nil && *c.Logo != "" {
132 if len(*c.Logo) > 1024 {
133 errors = append(errors, ErrClientLogoTooLong)
135 u, err := url.Parse(*c.Logo)
136 if err != nil || !u.IsAbs() {
137 errors = append(errors, ErrClientLogoNotURL)
140 if c.Website != nil && *c.Website != "" {
141 if len(*c.Website) > 140 {
142 errors = append(errors, ErrClientWebsiteTooLong)
144 u, err := url.Parse(*c.Website)
145 if err != nil || !u.IsAbs() {
146 errors = append(errors, ErrClientWebsiteNotURL)
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()
156 clientIDStr = r.PostFormValue("client_id")
158 if clientIDStr == "" {
159 w.WriteHeader(http.StatusUnauthorized)
161 w.Header().Set("WWW-Authenticate", "Basic")
163 renderJSONError(enc, "invalid_client")
164 return nil, "", false
166 if !allowPublic && !fromAuthHeader {
167 w.WriteHeader(http.StatusBadRequest)
168 renderJSONError(enc, "unauthorized_client")
169 return nil, "", false
171 clientID, err := uuid.Parse(clientIDStr)
173 log.Println("Error decoding client ID:", err)
174 w.WriteHeader(http.StatusUnauthorized)
176 w.Header().Set("WWW-Authenticate", "Basic")
178 renderJSONError(enc, "invalid_client")
179 return nil, "", false
181 return clientID, clientSecret, true
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)
190 _, _, fromAuthHeader := r.BasicAuth()
191 client, err := context.GetClient(clientID)
192 if err == ErrClientNotFound {
193 w.WriteHeader(http.StatusUnauthorized)
195 w.Header().Set("WWW-Authenticate", "Basic")
197 renderJSONError(enc, "invalid_client")
199 } else if err != nil {
200 w.WriteHeader(http.StatusInternalServerError)
201 renderJSONError(enc, "server_error")
204 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
205 w.WriteHeader(http.StatusUnauthorized)
207 w.Header().Set("WWW-Authenticate", "Basic")
209 renderJSONError(enc, "invalid_client")
212 return clientID, true
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"`
227 func normalizeURIString(in string) (string, error) {
228 n, err := purell.NormalizeURLString(in, normalizeFlags)
231 return in, ErrEndpointURINotURL
236 func normalizeURI(in *url.URL) string {
237 return purell.NormalizeURL(in, normalizeFlags)
240 type sortedEndpoints []Endpoint
242 func (s sortedEndpoints) Len() int {
246 func (s sortedEndpoints) Less(i, j int) bool {
247 return s[i].Added.Before(s[j].Added)
250 func (s sortedEndpoints) Swap(i, j int) {
251 s[i], s[j] = s[j], s[i]
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)
269 func (m *memstore) getClient(id uuid.ID) (Client, error) {
271 defer m.clientLock.RUnlock()
272 c, ok := m.clients[id.String()]
274 return Client{}, ErrClientNotFound
279 func (m *memstore) saveClient(client Client) error {
281 defer m.clientLock.Unlock()
282 if _, ok := m.clients[client.ID.String()]; ok {
283 return ErrClientAlreadyExists
285 m.clients[client.ID.String()] = client
286 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
290 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
292 defer m.clientLock.Unlock()
293 c, ok := m.clients[id.String()]
295 return ErrClientNotFound
297 c.ApplyChange(change)
298 m.clients[id.String()] = c
302 func (m *memstore) deleteClient(id uuid.ID) error {
303 client, err := m.getClient(id)
308 defer m.clientLock.Unlock()
309 delete(m.clients, id.String())
311 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
318 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
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 {
330 return []Client{}, nil
332 clients := []Client{}
333 for _, id := range ids {
334 client, err := m.getClient(id)
336 return []Client{}, err
338 clients = append(clients, client)
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...)
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) {
358 return Endpoint{}, ErrEndpointNotFound
361 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
362 m.endpointLock.Lock()
363 defer m.endpointLock.Unlock()
365 for p, item := range m.endpoints[client.String()] {
366 if item.ID.Equal(endpoint) {
372 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
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 {
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
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
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"`
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")
419 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
420 errors := []requestError{}
421 username, password, ok := r.BasicAuth()
423 errors = append(errors, requestError{Slug: requestErrAccessDenied})
424 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
427 profile, err := authenticate(username, password, c)
429 if isAuthError(err) {
430 errors = append(errors, requestError{Slug: requestErrAccessDenied})
431 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
433 log.Printf("Error authenticating: %#+v\n", err)
434 errors = append(errors, requestError{Slug: requestErrActOfGod})
435 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
440 decoder := json.NewDecoder(r.Body)
441 err = decoder.Decode(&req)
443 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
447 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
448 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
449 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
452 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
453 } else if len(req.Name) < minClientNameLen {
454 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
455 } else if len(req.Name) > maxClientNameLen {
456 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
459 encode(w, r, http.StatusBadRequest, response{Errors: errors})
467 Website: req.Website,
470 if client.Type == clientTypeConfidential {
471 secret := make([]byte, 32)
472 _, err = rand.Read(secret)
474 log.Printf("Error generating secret: %#+v\n", err)
475 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
478 client.Secret = hex.EncodeToString(secret)
480 err = c.SaveClient(client)
482 if err == ErrClientAlreadyExists {
483 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
484 encode(w, r, http.StatusBadRequest, response{Errors: errors})
487 log.Printf("Error saving client: %#+v\n", err)
488 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
491 endpoints := []Endpoint{}
492 for pos, u := range req.Endpoints {
493 uri, err := url.Parse(u)
495 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
499 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
502 endpoint := Endpoint{
508 endpoints = append(endpoints, endpoint)
510 err = c.AddEndpoints(client.ID, endpoints)
512 log.Printf("Error adding endpoints: %#+v\n", err)
513 errors = append(errors, requestError{Slug: requestErrActOfGod})
514 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
518 Clients: []Client{client},
519 Endpoints: endpoints,
522 encode(w, r, http.StatusCreated, resp)
525 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
526 errors := []requestError{}
528 if vars["id"] == "" {
529 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
530 encode(w, r, http.StatusBadRequest, response{Errors: errors})
533 id, err := uuid.Parse(vars["id"])
535 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
536 encode(w, r, http.StatusBadRequest, response{Errors: errors})
539 client, err := c.GetClient(id)
541 if err == ErrClientNotFound {
542 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
543 encode(w, r, http.StatusNotFound, response{Errors: errors})
546 errors = append(errors, requestError{Slug: requestErrActOfGod})
547 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
550 username, password, ok := r.BasicAuth()
554 profile, err := authenticate(username, password, c)
556 if isAuthError(err) {
557 errors = append(errors, requestError{Slug: requestErrAccessDenied})
558 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
560 errors = append(errors, requestError{Slug: requestErrActOfGod})
561 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
565 if !client.OwnerID.Equal(profile.ID) {
570 Clients: []Client{client},
573 encode(w, r, http.StatusOK, resp)
576 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
577 errors := []requestError{}
579 // BUG(paddy): If ids are provided in query params, retrieve only those clients
580 num := defaultClientResponseSize
582 ownerIDStr := r.URL.Query().Get("owner_id")
583 numStr := r.URL.Query().Get("num")
584 offsetStr := r.URL.Query().Get("offset")
586 num, err = strconv.Atoi(numStr)
588 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
590 if num > maxClientResponseSize {
591 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
595 offset, err = strconv.Atoi(offsetStr)
597 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
600 if ownerIDStr == "" {
601 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
604 encode(w, r, http.StatusBadRequest, response{Errors: errors})
607 ownerID, err := uuid.Parse(ownerIDStr)
609 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
610 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
613 clients, err := c.ListClientsByOwner(ownerID, num, offset)
615 errors = append(errors, requestError{Slug: requestErrActOfGod})
616 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
619 username, password, ok := r.BasicAuth()
621 for pos, client := range clients {
623 clients[pos] = client
626 profile, err := authenticate(username, password, c)
628 if isAuthError(err) {
629 errors = append(errors, requestError{Slug: requestErrAccessDenied})
630 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
632 errors = append(errors, requestError{Slug: requestErrActOfGod})
633 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
637 for pos, client := range clients {
638 if !client.OwnerID.Equal(profile.ID) {
640 clients[pos] = client
648 encode(w, r, http.StatusOK, resp)
651 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
652 errors := []requestError{}
654 if _, ok := vars["id"]; !ok {
655 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
656 encode(w, r, http.StatusBadRequest, response{Errors: errors})
659 id, err := uuid.Parse(vars["id"])
661 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
663 username, password, ok := r.BasicAuth()
665 errors = append(errors, requestError{Slug: requestErrAccessDenied})
666 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
669 profile, err := authenticate(username, password, c)
671 if isAuthError(err) {
672 errors = append(errors, requestError{Slug: requestErrAccessDenied})
673 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
675 errors = append(errors, requestError{Slug: requestErrActOfGod})
676 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
680 var change ClientChange
681 err = decode(r, &change)
683 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
684 encode(w, r, http.StatusBadRequest, response{Errors: errors})
687 errs := change.Validate()
688 for _, err := range errs {
691 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"})
692 case ErrClientNameTooShort:
693 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
694 case ErrClientNameTooLong:
695 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
696 case ErrClientLogoTooLong:
697 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"})
698 case ErrClientLogoNotURL:
699 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"})
700 case ErrClientWebsiteTooLong:
701 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"})
702 case ErrClientWebsiteNotURL:
703 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"})
705 log.Println("Unrecognised error from client change validation:", err)
709 encode(w, r, http.StatusBadRequest, response{Errors: errors})
712 client, err := c.GetClient(id)
713 if err == ErrClientNotFound {
714 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
715 encode(w, r, http.StatusNotFound, response{Errors: errors})
717 } else if err != nil {
718 log.Println("Error retrieving client:", err)
719 errors = append(errors, requestError{Slug: requestErrActOfGod})
720 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
723 if !client.OwnerID.Equal(profile.ID) {
724 errors = append(errors, requestError{Slug: requestErrAccessDenied})
725 encode(w, r, http.StatusForbidden, response{Errors: errors})
728 if change.Secret != nil && client.Type == clientTypeConfidential {
729 secret := make([]byte, 32)
730 _, err = rand.Read(secret)
732 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
735 newSecret := hex.EncodeToString(secret)
736 change.Secret = &newSecret
738 err = c.UpdateClient(id, change)
740 log.Println("Error updating client:", err)
741 errors = append(errors, requestError{Slug: requestErrActOfGod})
742 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
745 client.ApplyChange(change)
746 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
750 func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
751 errors := []requestError{}
753 if _, ok := vars["id"]; !ok {
754 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
755 encode(w, r, http.StatusNotFound, response{Errors: errors})
758 id, err := uuid.Parse(vars["id"])
760 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
762 username, password, ok := r.BasicAuth()
764 errors = append(errors, requestError{Slug: requestErrAccessDenied})
765 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
768 profile, err := authenticate(username, password, c)
770 if isAuthError(err) {
771 errors = append(errors, requestError{Slug: requestErrAccessDenied})
772 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
774 errors = append(errors, requestError{Slug: requestErrActOfGod})
775 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
779 client, err := c.GetClient(id)
781 if err == ErrClientNotFound {
782 errors = append(errors, requestError{Slug: requestErrNotFound})
783 encode(w, r, http.StatusNotFound, response{Errors: errors})
786 log.Println("Error retrieving client:", err)
787 errors = append(errors, requestError{Slug: requestErrActOfGod})
788 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
791 if !client.OwnerID.Equal(profile.ID) {
792 errors = append(errors, requestError{Slug: requestErrAccessDenied})
793 encode(w, r, http.StatusForbidden, response{Errors: errors})
796 err = c.DeleteClient(id)
798 if err == ErrClientNotFound {
799 errors = append(errors, requestError{Slug: requestErrNotFound})
800 encode(w, r, http.StatusNotFound, response{Errors: errors})
803 log.Println("Error deleting client:", err)
804 errors = append(errors, requestError{Slug: requestErrActOfGod})
805 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
808 // BUG(paddy): Client needs to clean up after itself, invalidating tokens, deleting unused grants, deleting endpoints
809 encode(w, r, http.StatusOK, response{Errors: errors})
813 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
814 type addEndpointReq struct {
815 Endpoints []string `json:"endpoints"`
817 errors := []requestError{}
819 if vars["id"] == "" {
820 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
821 encode(w, r, http.StatusBadRequest, response{Errors: errors})
824 id, err := uuid.Parse(vars["id"])
826 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
827 encode(w, r, http.StatusBadRequest, response{Errors: errors})
830 username, password, ok := r.BasicAuth()
832 errors = append(errors, requestError{Slug: requestErrAccessDenied})
833 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
836 profile, err := authenticate(username, password, c)
838 if isAuthError(err) {
839 errors = append(errors, requestError{Slug: requestErrAccessDenied})
840 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
842 errors = append(errors, requestError{Slug: requestErrActOfGod})
843 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
847 client, err := c.GetClient(id)
849 if err == ErrClientNotFound {
850 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
851 encode(w, r, http.StatusBadRequest, response{Errors: errors})
854 errors = append(errors, requestError{Slug: requestErrActOfGod})
855 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
858 if !client.OwnerID.Equal(profile.ID) {
859 errors = append(errors, requestError{Slug: requestErrAccessDenied})
860 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
863 var req addEndpointReq
864 decoder := json.NewDecoder(r.Body)
865 err = decoder.Decode(&req)
867 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
870 if len(req.Endpoints) < 1 {
871 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"})
872 encode(w, r, http.StatusBadRequest, response{Errors: errors})
875 endpoints := []Endpoint{}
876 for pos, u := range req.Endpoints {
877 if parsed, err := url.Parse(u); err != nil {
878 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
880 } else if !parsed.IsAbs() {
881 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
890 endpoints = append(endpoints, e)
893 encode(w, r, http.StatusBadRequest, response{Errors: errors})
896 err = c.AddEndpoints(id, endpoints)
898 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
903 Endpoints: endpoints,
905 encode(w, r, http.StatusCreated, resp)
908 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
909 errors := []requestError{}
911 clientID, err := uuid.Parse(vars["id"])
913 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
914 encode(w, r, http.StatusBadRequest, response{Errors: errors})
917 num := defaultEndpointResponseSize
919 numStr := r.URL.Query().Get("num")
920 offsetStr := r.URL.Query().Get("offset")
922 num, err = strconv.Atoi(numStr)
924 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
926 if num > maxEndpointResponseSize {
927 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
931 offset, err = strconv.Atoi(offsetStr)
933 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
937 encode(w, r, http.StatusBadRequest, response{Errors: errors})
940 endpoints, err := c.ListEndpoints(clientID, num, offset)
942 errors = append(errors, requestError{Slug: requestErrActOfGod})
943 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
947 Endpoints: endpoints,
950 encode(w, r, http.StatusOK, resp)
953 func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) {
954 errors := []requestError{}
956 if vars["client_id"] == "" {
957 errors = append(errors, requestError{Slug: requestErrMissing, Param: "client_id"})
958 encode(w, r, http.StatusBadRequest, response{Errors: errors})
961 clientID, err := uuid.Parse(vars["client_id"])
963 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
964 encode(w, r, http.StatusBadRequest, response{Errors: errors})
967 if vars["id"] == "" {
968 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
969 encode(w, r, http.StatusBadRequest, response{Errors: errors})
972 id, err := uuid.Parse(vars["id"])
974 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
975 encode(w, r, http.StatusBadRequest, response{Errors: errors})
978 username, password, ok := r.BasicAuth()
980 errors = append(errors, requestError{Slug: requestErrAccessDenied})
981 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
984 profile, err := authenticate(username, password, c)
986 if isAuthError(err) {
987 errors = append(errors, requestError{Slug: requestErrAccessDenied})
988 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
990 errors = append(errors, requestError{Slug: requestErrActOfGod})
991 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
995 client, err := c.GetClient(clientID)
997 if err == ErrClientNotFound {
998 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "client_id"})
999 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1002 errors = append(errors, requestError{Slug: requestErrActOfGod})
1003 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1006 if !client.OwnerID.Equal(profile.ID) {
1007 errors = append(errors, requestError{Slug: requestErrAccessDenied})
1008 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
1011 endpoint, err := c.GetEndpoint(clientID, id)
1013 if err == ErrEndpointNotFound {
1014 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
1015 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1018 errors = append(errors, requestError{Slug: requestErrActOfGod})
1019 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1022 err = c.RemoveEndpoint(clientID, id)
1024 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
1029 Endpoints: []Endpoint{endpoint},
1031 encode(w, r, http.StatusCreated, resp)
1034 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
1035 scopes = strings.Split(r.PostFormValue("scope"), " ")
1040 func clientCredentialsAuditString(r *http.Request) string {
1041 return "client_credentials"