auth

Paddy 2015-04-11 Parent:77db7c65216c Child:73e12d5a1124

160:48200d8c4036 Go to Latest

auth/client.go

Start to support deleting profiles through the API. Create a removeLoginsByProfile method on the profileStore, to allow an easy way to bulk-delete logins associated with a Profile after the Profile has been deleted. Create postgres and memstore implementations of the removeLoginsByProfile method. Create a cleanUpAfterProfileDeletion helper method that will clean up the child objects of a Profile (its Sessions, Tokens, Clients, etc.). The intended usage is to call this in a goroutine after a Profile has been deleted, to try and get things back in order. Detect when the UpdateProfileHandler API is used to set the Deleted flag of a Profile to true, and clean up after the Profile when that's the case. Add a DeleteProfileHandler API endpoint that is a shortcut to setting the Deleted flag of a Profile to true and cleaning up after the Profile. The problem with our approach thus far is that some of it is reversible and some is not. If a Profile is maliciously/accidentally deleted, it's simple enough to use the API as a superuser to restore the Profile. But doing that will not (and cannot) restore the Logins associated with that Profile, for example. While it would be nice to add a Deleted flag to our Logins that we could simply toggle, that would wreak havoc with our database constraints and ensuring uniqueness of Login values. I still don't have a solution for this, outside the superuser manually restoring a Login for the Profile, after which the user can authenticate themselves and add more Logins as desired. But there has to be a better way. I suppose since the passphrase is being stored with the Profile and not the Login, we could offer an endpoint that would automate this, but... well, that would be tricky. It would require the user remembering their Profile ID, and let's be honest, nobody's going to remember a UUID. Maybe such an endpoint would help from a customer service standpoint: we identify their Profile manually, then send them to /profiles/ID/restorelogin or something, and that lets them add a Login back to the Profile. I'll figure it out later. For now, we know we at least have enough information to identify a user is who they say they are and resolve the situation manually.

History
1 package auth
3 import (
4 "crypto/rand"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "log"
9 "net/http"
10 "net/url"
11 "strconv"
12 "strings"
13 "time"
15 "github.com/PuerkitoBio/purell"
16 "github.com/gorilla/mux"
18 "code.secondbit.org/uuid.hg"
19 )
21 func init() {
22 RegisterGrantType("client_credentials", GrantType{
23 Validate: clientCredentialsValidate,
24 Invalidate: nil,
25 IssuesRefresh: true,
26 ReturnToken: RenderJSONToken,
27 AllowsPublic: false,
28 AuditString: clientCredentialsAuditString,
29 })
30 }
32 var (
33 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first.
34 ErrNoClientStore = errors.New("no clientStore was specified for the Context")
35 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore.
36 ErrClientNotFound = errors.New("client not found in clientStore")
37 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with
38 // the same ID already exists in the clientStore.
39 ErrClientAlreadyExists = errors.New("client already exists in clientStore")
40 // ErrEndpointNotFound is returned when an Endpoint is requested but not found in a clientSTore.
41 ErrEndpointNotFound = errors.New("endpoint not found in clientStore")
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")
62 )
64 const (
65 clientTypePublic = "public"
66 clientTypeConfidential = "confidential"
67 minClientNameLen = 2
68 maxClientNameLen = 24
69 defaultClientResponseSize = 20
70 maxClientResponseSize = 50
71 defaultEndpointResponseSize = 20
72 maxEndpointResponseSize = 50
74 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery
75 )
77 // Client represents a client that grants access
78 // to the auth server, exchanging grants for tokens,
79 // and tokens for access.
80 type Client struct {
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"`
89 }
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
96 }
97 if change.OwnerID != nil {
98 c.OwnerID = change.OwnerID
99 }
100 if change.Name != nil {
101 c.Name = *change.Name
102 }
103 if change.Logo != nil {
104 c.Logo = *change.Logo
105 }
106 if change.Website != nil {
107 c.Website = *change.Website
108 }
109 if change.Deleted != nil {
110 c.Deleted = *change.Deleted
111 }
112 }
114 // ClientChange represents a bundle of options for
115 // updating a Client's mutable data.
116 type ClientChange struct {
117 Secret *string
118 OwnerID uuid.ID
119 Name *string
120 Logo *string
121 Website *string
122 Deleted *bool
123 }
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
127 }
129 // Validate checks the ClientChange it is called on
130 // and asserts its internal validity, or lack thereof.
131 func (c ClientChange) Validate() []error {
132 errors := []error{}
133 if c.Empty() {
134 errors = append(errors, ErrEmptyChange)
135 return errors
136 }
137 if c.Name != nil && len(*c.Name) < 2 {
138 errors = append(errors, ErrClientNameTooShort)
139 }
140 if c.Name != nil && len(*c.Name) > 32 {
141 errors = append(errors, ErrClientNameTooLong)
142 }
143 if c.Logo != nil && *c.Logo != "" {
144 if len(*c.Logo) > 1024 {
145 errors = append(errors, ErrClientLogoTooLong)
146 }
147 u, err := url.Parse(*c.Logo)
148 if err != nil || !u.IsAbs() {
149 errors = append(errors, ErrClientLogoNotURL)
150 }
151 }
152 if c.Website != nil && *c.Website != "" {
153 if len(*c.Website) > 140 {
154 errors = append(errors, ErrClientWebsiteTooLong)
155 }
156 u, err := url.Parse(*c.Website)
157 if err != nil || !u.IsAbs() {
158 errors = append(errors, ErrClientWebsiteNotURL)
159 }
160 }
161 return errors
162 }
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()
167 if !fromAuthHeader {
168 clientIDStr = r.PostFormValue("client_id")
169 }
170 if clientIDStr == "" {
171 w.WriteHeader(http.StatusUnauthorized)
172 if fromAuthHeader {
173 w.Header().Set("WWW-Authenticate", "Basic")
174 }
175 renderJSONError(enc, "invalid_client")
176 return nil, "", false
177 }
178 if !allowPublic && !fromAuthHeader {
179 w.WriteHeader(http.StatusBadRequest)
180 renderJSONError(enc, "unauthorized_client")
181 return nil, "", false
182 }
183 clientID, err := uuid.Parse(clientIDStr)
184 if err != nil {
185 log.Println("Error decoding client ID:", err)
186 w.WriteHeader(http.StatusUnauthorized)
187 if fromAuthHeader {
188 w.Header().Set("WWW-Authenticate", "Basic")
189 }
190 renderJSONError(enc, "invalid_client")
191 return nil, "", false
192 }
193 return clientID, clientSecret, true
194 }
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)
199 if !ok {
200 return nil, false
201 }
202 _, _, fromAuthHeader := r.BasicAuth()
203 client, err := context.GetClient(clientID)
204 if err == ErrClientNotFound {
205 w.WriteHeader(http.StatusUnauthorized)
206 if fromAuthHeader {
207 w.Header().Set("WWW-Authenticate", "Basic")
208 }
209 renderJSONError(enc, "invalid_client")
210 return nil, false
211 } else if err != nil {
212 w.WriteHeader(http.StatusInternalServerError)
213 renderJSONError(enc, "server_error")
214 return nil, false
215 }
216 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
217 w.WriteHeader(http.StatusUnauthorized)
218 if fromAuthHeader {
219 w.Header().Set("WWW-Authenticate", "Basic")
220 }
221 renderJSONError(enc, "invalid_client")
222 return nil, false
223 }
224 return clientID, true
225 }
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"`
237 }
239 func normalizeURIString(in string) (string, error) {
240 n, err := purell.NormalizeURLString(in, normalizeFlags)
241 if err != nil {
242 log.Println(err)
243 return in, ErrEndpointURINotURL
244 }
245 return n, nil
246 }
248 func normalizeURI(in *url.URL) string {
249 return purell.NormalizeURL(in, normalizeFlags)
250 }
252 type sortedEndpoints []Endpoint
254 func (s sortedEndpoints) Len() int {
255 return len(s)
256 }
258 func (s sortedEndpoints) Less(i, j int) bool {
259 return s[i].Added.Before(s[j].Added)
260 }
262 func (s sortedEndpoints) Swap(i, j int) {
263 s[i], s[j] = s[j], s[i]
264 }
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)
278 }
280 func (m *memstore) getClient(id uuid.ID) (Client, error) {
281 m.clientLock.RLock()
282 defer m.clientLock.RUnlock()
283 c, ok := m.clients[id.String()]
284 if !ok || c.Deleted {
285 return Client{}, ErrClientNotFound
286 }
287 return c, nil
288 }
290 func (m *memstore) saveClient(client Client) error {
291 m.clientLock.Lock()
292 defer m.clientLock.Unlock()
293 if _, ok := m.clients[client.ID.String()]; ok {
294 return ErrClientAlreadyExists
295 }
296 m.clients[client.ID.String()] = client
297 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
298 return nil
299 }
301 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
302 m.clientLock.Lock()
303 defer m.clientLock.Unlock()
304 c, ok := m.clients[id.String()]
305 if !ok {
306 return ErrClientNotFound
307 }
308 c.ApplyChange(change)
309 m.clients[id.String()] = c
310 return nil
311 }
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 {
318 ids = ids[offset:]
319 } else {
320 return []Client{}, nil
321 }
322 clients := []Client{}
323 for _, id := range ids {
324 client, err := m.getClient(id)
325 if err != nil {
326 if err == ErrClientNotFound {
327 continue
328 }
329 return []Client{}, err
330 }
331 clients = append(clients, client)
332 }
333 return clients, nil
334 }
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)
342 }
343 for client, e := range clients {
344 m.endpoints[client] = append(m.endpoints[client], e...)
345 }
346 return nil
347 }
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) {
354 return item, nil
355 }
356 }
357 return Endpoint{}, ErrEndpointNotFound
358 }
360 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
361 m.endpointLock.Lock()
362 defer m.endpointLock.Unlock()
363 pos := -1
364 for p, item := range m.endpoints[client.String()] {
365 if item.ID.Equal(endpoint) {
366 pos = p
367 break
368 }
369 }
370 if pos >= 0 {
371 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
372 }
373 return nil
374 }
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 {
381 return true, nil
382 }
383 }
384 return false, nil
385 }
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
391 }
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
397 }
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"`
405 }
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")
416 }
418 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
419 errors := []requestError{}
420 username, password, ok := r.BasicAuth()
421 if !ok {
422 errors = append(errors, requestError{Slug: requestErrAccessDenied})
423 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
424 return
425 }
426 profile, err := authenticate(username, password, c)
427 if err != nil {
428 if isAuthError(err) {
429 errors = append(errors, requestError{Slug: requestErrAccessDenied})
430 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
431 } else {
432 log.Printf("Error authenticating: %#+v\n", err)
433 errors = append(errors, requestError{Slug: requestErrActOfGod})
434 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
435 }
436 return
437 }
438 var req newClientReq
439 decoder := json.NewDecoder(r.Body)
440 err = decoder.Decode(&req)
441 if err != nil {
442 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
443 return
444 }
445 if req.Type == "" {
446 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
447 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
448 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
449 }
450 if req.Name == "" {
451 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
452 } else if len(req.Name) < minClientNameLen {
453 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
454 } else if len(req.Name) > maxClientNameLen {
455 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
456 }
457 if len(errors) > 0 {
458 encode(w, r, http.StatusBadRequest, response{Errors: errors})
459 return
460 }
461 client := Client{
462 ID: uuid.NewID(),
463 OwnerID: profile.ID,
464 Name: req.Name,
465 Logo: req.Logo,
466 Website: req.Website,
467 Type: req.Type,
468 }
469 if client.Type == clientTypeConfidential {
470 secret := make([]byte, 32)
471 _, err = rand.Read(secret)
472 if err != nil {
473 log.Printf("Error generating secret: %#+v\n", err)
474 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
475 return
476 }
477 client.Secret = hex.EncodeToString(secret)
478 }
479 err = c.SaveClient(client)
480 if err != nil {
481 if err == ErrClientAlreadyExists {
482 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
483 encode(w, r, http.StatusBadRequest, response{Errors: errors})
484 return
485 }
486 log.Printf("Error saving client: %#+v\n", err)
487 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
488 return
489 }
490 endpoints := []Endpoint{}
491 for pos, u := range req.Endpoints {
492 uri, err := url.Parse(u)
493 if err != nil {
494 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
495 continue
496 }
497 if !uri.IsAbs() {
498 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
499 continue
500 }
501 endpoint := Endpoint{
502 ID: uuid.NewID(),
503 ClientID: client.ID,
504 URI: uri.String(),
505 Added: time.Now(),
506 }
507 endpoints = append(endpoints, endpoint)
508 }
509 err = c.AddEndpoints(endpoints)
510 if err != nil {
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}})
514 return
515 }
516 resp := response{
517 Clients: []Client{client},
518 Endpoints: endpoints,
519 Errors: errors,
520 }
521 encode(w, r, http.StatusCreated, resp)
522 }
524 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
525 errors := []requestError{}
526 vars := mux.Vars(r)
527 if vars["id"] == "" {
528 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
529 encode(w, r, http.StatusBadRequest, response{Errors: errors})
530 return
531 }
532 id, err := uuid.Parse(vars["id"])
533 if err != nil {
534 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
535 encode(w, r, http.StatusBadRequest, response{Errors: errors})
536 return
537 }
538 client, err := c.GetClient(id)
539 if err != nil {
540 if err == ErrClientNotFound {
541 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
542 encode(w, r, http.StatusNotFound, response{Errors: errors})
543 return
544 }
545 errors = append(errors, requestError{Slug: requestErrActOfGod})
546 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
547 return
548 }
549 username, password, ok := r.BasicAuth()
550 if !ok {
551 client.Secret = ""
552 } else {
553 profile, err := authenticate(username, password, c)
554 if err != nil {
555 if isAuthError(err) {
556 errors = append(errors, requestError{Slug: requestErrAccessDenied})
557 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
558 } else {
559 errors = append(errors, requestError{Slug: requestErrActOfGod})
560 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
561 }
562 return
563 }
564 if !client.OwnerID.Equal(profile.ID) {
565 client.Secret = ""
566 }
567 }
568 resp := response{
569 Clients: []Client{client},
570 Errors: errors,
571 }
572 encode(w, r, http.StatusOK, resp)
573 }
575 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
576 errors := []requestError{}
577 var err error
578 // BUG(paddy): If ids are provided in query params, retrieve only those clients
579 num := defaultClientResponseSize
580 offset := 0
581 ownerIDStr := r.URL.Query().Get("owner_id")
582 numStr := r.URL.Query().Get("num")
583 offsetStr := r.URL.Query().Get("offset")
584 if numStr != "" {
585 num, err = strconv.Atoi(numStr)
586 if err != nil {
587 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
588 }
589 if num > maxClientResponseSize {
590 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
591 }
592 }
593 if offsetStr != "" {
594 offset, err = strconv.Atoi(offsetStr)
595 if err != nil {
596 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
597 }
598 }
599 if ownerIDStr == "" {
600 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
601 }
602 if len(errors) > 0 {
603 encode(w, r, http.StatusBadRequest, response{Errors: errors})
604 return
605 }
606 ownerID, err := uuid.Parse(ownerIDStr)
607 if err != nil {
608 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
609 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
610 return
611 }
612 clients, err := c.ListClientsByOwner(ownerID, num, offset)
613 if err != nil {
614 errors = append(errors, requestError{Slug: requestErrActOfGod})
615 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
616 return
617 }
618 username, password, ok := r.BasicAuth()
619 if !ok {
620 for pos, client := range clients {
621 client.Secret = ""
622 clients[pos] = client
623 }
624 } else {
625 profile, err := authenticate(username, password, c)
626 if err != nil {
627 if isAuthError(err) {
628 errors = append(errors, requestError{Slug: requestErrAccessDenied})
629 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
630 } else {
631 errors = append(errors, requestError{Slug: requestErrActOfGod})
632 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
633 }
634 return
635 }
636 for pos, client := range clients {
637 if !client.OwnerID.Equal(profile.ID) {
638 client.Secret = ""
639 clients[pos] = client
640 }
641 }
642 }
643 resp := response{
644 Clients: clients,
645 Errors: errors,
646 }
647 encode(w, r, http.StatusOK, resp)
648 }
650 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
651 errors := []requestError{}
652 vars := mux.Vars(r)
653 if _, ok := vars["id"]; !ok {
654 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
655 encode(w, r, http.StatusBadRequest, response{Errors: errors})
656 return
657 }
658 id, err := uuid.Parse(vars["id"])
659 if err != nil {
660 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
661 }
662 username, password, ok := r.BasicAuth()
663 if !ok {
664 errors = append(errors, requestError{Slug: requestErrAccessDenied})
665 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
666 return
667 }
668 profile, err := authenticate(username, password, c)
669 if err != nil {
670 if isAuthError(err) {
671 errors = append(errors, requestError{Slug: requestErrAccessDenied})
672 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
673 } else {
674 errors = append(errors, requestError{Slug: requestErrActOfGod})
675 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
676 }
677 return
678 }
679 var change ClientChange
680 err = decode(r, &change)
681 if err != nil {
682 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
683 encode(w, r, http.StatusBadRequest, response{Errors: errors})
684 return
685 }
686 errs := change.Validate()
687 for _, err := range errs {
688 switch err {
689 case ErrEmptyChange:
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"})
703 default:
704 log.Println("Unrecognised error from client change validation:", err)
705 }
706 }
707 if len(errors) > 0 {
708 encode(w, r, http.StatusBadRequest, response{Errors: errors})
709 return
710 }
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})
715 return
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})
720 return
721 }
722 if !client.OwnerID.Equal(profile.ID) {
723 errors = append(errors, requestError{Slug: requestErrAccessDenied})
724 encode(w, r, http.StatusForbidden, response{Errors: errors})
725 return
726 }
727 if change.Secret != nil && client.Type == clientTypeConfidential {
728 secret := make([]byte, 32)
729 _, err = rand.Read(secret)
730 if err != nil {
731 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
732 return
733 }
734 newSecret := hex.EncodeToString(secret)
735 change.Secret = &newSecret
736 }
737 err = c.UpdateClient(id, change)
738 if err != nil {
739 log.Println("Error updating client:", err)
740 errors = append(errors, requestError{Slug: requestErrActOfGod})
741 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
742 return
743 }
744 client.ApplyChange(change)
745 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
746 return
747 }
749 func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
750 errors := []requestError{}
751 vars := mux.Vars(r)
752 if _, ok := vars["id"]; !ok {
753 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
754 encode(w, r, http.StatusNotFound, response{Errors: errors})
755 return
756 }
757 id, err := uuid.Parse(vars["id"])
758 if err != nil {
759 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
760 }
761 username, password, ok := r.BasicAuth()
762 if !ok {
763 errors = append(errors, requestError{Slug: requestErrAccessDenied})
764 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
765 return
766 }
767 profile, err := authenticate(username, password, c)
768 if err != nil {
769 if isAuthError(err) {
770 errors = append(errors, requestError{Slug: requestErrAccessDenied})
771 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
772 } else {
773 errors = append(errors, requestError{Slug: requestErrActOfGod})
774 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
775 }
776 return
777 }
778 client, err := c.GetClient(id)
779 if err != nil {
780 if err == ErrClientNotFound {
781 errors = append(errors, requestError{Slug: requestErrNotFound})
782 encode(w, r, http.StatusNotFound, response{Errors: errors})
783 return
784 }
785 log.Println("Error retrieving client:", err)
786 errors = append(errors, requestError{Slug: requestErrActOfGod})
787 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
788 return
789 }
790 if !client.OwnerID.Equal(profile.ID) {
791 errors = append(errors, requestError{Slug: requestErrAccessDenied})
792 encode(w, r, http.StatusForbidden, response{Errors: errors})
793 return
794 }
795 deleted := true
796 change := ClientChange{Deleted: &deleted}
797 err = c.UpdateClient(id, change)
798 if err != nil {
799 if err == ErrClientNotFound {
800 errors = append(errors, requestError{Slug: requestErrNotFound})
801 encode(w, r, http.StatusNotFound, response{Errors: errors})
802 return
803 }
804 log.Println("Error deleting client:", err)
805 errors = append(errors, requestError{Slug: requestErrActOfGod})
806 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
807 return
808 }
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})
811 return
812 }
814 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
815 type addEndpointReq struct {
816 Endpoints []string `json:"endpoints"`
817 }
818 errors := []requestError{}
819 vars := mux.Vars(r)
820 if vars["id"] == "" {
821 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
822 encode(w, r, http.StatusBadRequest, response{Errors: errors})
823 return
824 }
825 id, err := uuid.Parse(vars["id"])
826 if err != nil {
827 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
828 encode(w, r, http.StatusBadRequest, response{Errors: errors})
829 return
830 }
831 username, password, ok := r.BasicAuth()
832 if !ok {
833 errors = append(errors, requestError{Slug: requestErrAccessDenied})
834 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
835 return
836 }
837 profile, err := authenticate(username, password, c)
838 if err != nil {
839 if isAuthError(err) {
840 errors = append(errors, requestError{Slug: requestErrAccessDenied})
841 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
842 } else {
843 errors = append(errors, requestError{Slug: requestErrActOfGod})
844 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
845 }
846 return
847 }
848 client, err := c.GetClient(id)
849 if err != nil {
850 if err == ErrClientNotFound {
851 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
852 encode(w, r, http.StatusBadRequest, response{Errors: errors})
853 return
854 }
855 errors = append(errors, requestError{Slug: requestErrActOfGod})
856 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
857 return
858 }
859 if !client.OwnerID.Equal(profile.ID) {
860 errors = append(errors, requestError{Slug: requestErrAccessDenied})
861 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
862 return
863 }
864 var req addEndpointReq
865 decoder := json.NewDecoder(r.Body)
866 err = decoder.Decode(&req)
867 if err != nil {
868 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
869 return
870 }
871 if len(req.Endpoints) < 1 {
872 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"})
873 encode(w, r, http.StatusBadRequest, response{Errors: errors})
874 return
875 }
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)})
880 continue
881 } else if !parsed.IsAbs() {
882 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
883 continue
884 }
885 e := Endpoint{
886 ID: uuid.NewID(),
887 ClientID: id,
888 URI: u,
889 Added: time.Now(),
890 }
891 endpoints = append(endpoints, e)
892 }
893 if len(errors) > 0 {
894 encode(w, r, http.StatusBadRequest, response{Errors: errors})
895 return
896 }
897 err = c.AddEndpoints(endpoints)
898 if err != nil {
899 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
900 return
901 }
902 resp := response{
903 Errors: errors,
904 Endpoints: endpoints,
905 }
906 encode(w, r, http.StatusCreated, resp)
907 }
909 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
910 errors := []requestError{}
911 vars := mux.Vars(r)
912 clientID, err := uuid.Parse(vars["id"])
913 if err != nil {
914 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
915 encode(w, r, http.StatusBadRequest, response{Errors: errors})
916 return
917 }
918 num := defaultEndpointResponseSize
919 offset := 0
920 numStr := r.URL.Query().Get("num")
921 offsetStr := r.URL.Query().Get("offset")
922 if numStr != "" {
923 num, err = strconv.Atoi(numStr)
924 if err != nil {
925 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
926 }
927 if num > maxEndpointResponseSize {
928 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
929 }
930 }
931 if offsetStr != "" {
932 offset, err = strconv.Atoi(offsetStr)
933 if err != nil {
934 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
935 }
936 }
937 if len(errors) > 0 {
938 encode(w, r, http.StatusBadRequest, response{Errors: errors})
939 return
940 }
941 endpoints, err := c.ListEndpoints(clientID, num, offset)
942 if err != nil {
943 errors = append(errors, requestError{Slug: requestErrActOfGod})
944 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
945 return
946 }
947 resp := response{
948 Endpoints: endpoints,
949 Errors: errors,
950 }
951 encode(w, r, http.StatusOK, resp)
952 }
954 func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) {
955 errors := []requestError{}
956 vars := mux.Vars(r)
957 if vars["client_id"] == "" {
958 errors = append(errors, requestError{Slug: requestErrMissing, Param: "client_id"})
959 encode(w, r, http.StatusBadRequest, response{Errors: errors})
960 return
961 }
962 clientID, err := uuid.Parse(vars["client_id"])
963 if err != nil {
964 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
965 encode(w, r, http.StatusBadRequest, response{Errors: errors})
966 return
967 }
968 if vars["id"] == "" {
969 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
970 encode(w, r, http.StatusBadRequest, response{Errors: errors})
971 return
972 }
973 id, err := uuid.Parse(vars["id"])
974 if err != nil {
975 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
976 encode(w, r, http.StatusBadRequest, response{Errors: errors})
977 return
978 }
979 username, password, ok := r.BasicAuth()
980 if !ok {
981 errors = append(errors, requestError{Slug: requestErrAccessDenied})
982 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
983 return
984 }
985 profile, err := authenticate(username, password, c)
986 if err != nil {
987 if isAuthError(err) {
988 errors = append(errors, requestError{Slug: requestErrAccessDenied})
989 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
990 } else {
991 errors = append(errors, requestError{Slug: requestErrActOfGod})
992 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
993 }
994 return
995 }
996 client, err := c.GetClient(clientID)
997 if err != nil {
998 if err == ErrClientNotFound {
999 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "client_id"})
1000 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1001 return
1003 errors = append(errors, requestError{Slug: requestErrActOfGod})
1004 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1005 return
1007 if !client.OwnerID.Equal(profile.ID) {
1008 errors = append(errors, requestError{Slug: requestErrAccessDenied})
1009 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
1010 return
1012 endpoint, err := c.GetEndpoint(clientID, id)
1013 if err != nil {
1014 if err == ErrEndpointNotFound {
1015 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
1016 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1017 return
1019 errors = append(errors, requestError{Slug: requestErrActOfGod})
1020 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1021 return
1023 err = c.RemoveEndpoint(clientID, id)
1024 if err != nil {
1025 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
1026 return
1028 resp := response{
1029 Errors: errors,
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"), " ")
1037 valid = true
1038 return
1041 func clientCredentialsAuditString(r *http.Request) string {
1042 return "client_credentials"