auth

Paddy 2015-03-21 Parent:93c758f57c69 Child:77db7c65216c

149:8267e1c8bcd1 Go to Latest

auth/client.go

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.

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")
43 // ErrEmptyChange is returned when a Change has all its properties set to nil.
44 ErrEmptyChange = errors.New("change must have at least one property set")
45 // ErrClientNameTooShort is returned when a Client's Name property is too short.
46 ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
47 // ErrClientNameTooLong is returned when a Client's Name property is too long.
48 ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
49 // ErrClientLogoTooLong is returned when a Client's Logo property is too long.
50 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
51 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL.
52 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
53 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long.
54 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
55 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL.
56 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
57 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL.
58 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL")
59 )
61 const (
62 clientTypePublic = "public"
63 clientTypeConfidential = "confidential"
64 minClientNameLen = 2
65 maxClientNameLen = 24
66 defaultClientResponseSize = 20
67 maxClientResponseSize = 50
68 defaultEndpointResponseSize = 20
69 maxEndpointResponseSize = 50
71 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery
72 )
74 // Client represents a client that grants access
75 // to the auth server, exchanging grants for tokens,
76 // and tokens for access.
77 type Client struct {
78 ID uuid.ID `json:"id,omitempty"`
79 Secret string `json:"secret,omitempty"`
80 OwnerID uuid.ID `json:"owner_id,omitempty"`
81 Name string `json:"name,omitempty"`
82 Logo string `json:"logo,omitempty"`
83 Website string `json:"website,omitempty"`
84 Type string `json:"type,omitempty"`
85 }
87 // ApplyChange applies the properties of the passed
88 // ClientChange to the Client object it is called on.
89 func (c *Client) ApplyChange(change ClientChange) {
90 if change.Secret != nil {
91 c.Secret = *change.Secret
92 }
93 if change.OwnerID != nil {
94 c.OwnerID = change.OwnerID
95 }
96 if change.Name != nil {
97 c.Name = *change.Name
98 }
99 if change.Logo != nil {
100 c.Logo = *change.Logo
101 }
102 if change.Website != nil {
103 c.Website = *change.Website
104 }
105 }
107 // ClientChange represents a bundle of options for
108 // updating a Client's mutable data.
109 type ClientChange struct {
110 Secret *string
111 OwnerID uuid.ID
112 Name *string
113 Logo *string
114 Website *string
115 }
117 // Validate checks the ClientChange it is called on
118 // and asserts its internal validity, or lack thereof.
119 func (c ClientChange) Validate() []error {
120 errors := []error{}
121 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
122 errors = append(errors, ErrEmptyChange)
123 return errors
124 }
125 if c.Name != nil && len(*c.Name) < 2 {
126 errors = append(errors, ErrClientNameTooShort)
127 }
128 if c.Name != nil && len(*c.Name) > 32 {
129 errors = append(errors, ErrClientNameTooLong)
130 }
131 if c.Logo != nil && *c.Logo != "" {
132 if len(*c.Logo) > 1024 {
133 errors = append(errors, ErrClientLogoTooLong)
134 }
135 u, err := url.Parse(*c.Logo)
136 if err != nil || !u.IsAbs() {
137 errors = append(errors, ErrClientLogoNotURL)
138 }
139 }
140 if c.Website != nil && *c.Website != "" {
141 if len(*c.Website) > 140 {
142 errors = append(errors, ErrClientWebsiteTooLong)
143 }
144 u, err := url.Parse(*c.Website)
145 if err != nil || !u.IsAbs() {
146 errors = append(errors, ErrClientWebsiteNotURL)
147 }
148 }
149 return errors
150 }
152 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) {
153 enc := json.NewEncoder(w)
154 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
155 if !fromAuthHeader {
156 clientIDStr = r.PostFormValue("client_id")
157 }
158 if clientIDStr == "" {
159 w.WriteHeader(http.StatusUnauthorized)
160 if fromAuthHeader {
161 w.Header().Set("WWW-Authenticate", "Basic")
162 }
163 renderJSONError(enc, "invalid_client")
164 return nil, "", false
165 }
166 if !allowPublic && !fromAuthHeader {
167 w.WriteHeader(http.StatusBadRequest)
168 renderJSONError(enc, "unauthorized_client")
169 return nil, "", false
170 }
171 clientID, err := uuid.Parse(clientIDStr)
172 if err != nil {
173 log.Println("Error decoding client ID:", err)
174 w.WriteHeader(http.StatusUnauthorized)
175 if fromAuthHeader {
176 w.Header().Set("WWW-Authenticate", "Basic")
177 }
178 renderJSONError(enc, "invalid_client")
179 return nil, "", false
180 }
181 return clientID, clientSecret, true
182 }
184 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
185 enc := json.NewEncoder(w)
186 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic)
187 if !ok {
188 return nil, false
189 }
190 _, _, fromAuthHeader := r.BasicAuth()
191 client, err := context.GetClient(clientID)
192 if err == ErrClientNotFound {
193 w.WriteHeader(http.StatusUnauthorized)
194 if fromAuthHeader {
195 w.Header().Set("WWW-Authenticate", "Basic")
196 }
197 renderJSONError(enc, "invalid_client")
198 return nil, false
199 } else if err != nil {
200 w.WriteHeader(http.StatusInternalServerError)
201 renderJSONError(enc, "server_error")
202 return nil, false
203 }
204 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
205 w.WriteHeader(http.StatusUnauthorized)
206 if fromAuthHeader {
207 w.Header().Set("WWW-Authenticate", "Basic")
208 }
209 renderJSONError(enc, "invalid_client")
210 return nil, false
211 }
212 return clientID, true
213 }
215 // Endpoint represents a single URI that a Client
216 // controls. Users will be redirected to these URIs
217 // following successful authorization grants and
218 // exchanges for access tokens.
219 type Endpoint struct {
220 ID uuid.ID `json:"id,omitempty"`
221 ClientID uuid.ID `json:"client_id,omitempty"`
222 URI string `json:"uri,omitempty"`
223 NormalizedURI string `json:"-"`
224 Added time.Time `json:"added,omitempty"`
225 }
227 func normalizeURIString(in string) (string, error) {
228 n, err := purell.NormalizeURLString(in, normalizeFlags)
229 if err != nil {
230 log.Println(err)
231 return in, ErrEndpointURINotURL
232 }
233 return n, nil
234 }
236 func normalizeURI(in *url.URL) string {
237 return purell.NormalizeURL(in, normalizeFlags)
238 }
240 type sortedEndpoints []Endpoint
242 func (s sortedEndpoints) Len() int {
243 return len(s)
244 }
246 func (s sortedEndpoints) Less(i, j int) bool {
247 return s[i].Added.Before(s[j].Added)
248 }
250 func (s sortedEndpoints) Swap(i, j int) {
251 s[i], s[j] = s[j], s[i]
252 }
254 type clientStore interface {
255 getClient(id uuid.ID) (Client, error)
256 saveClient(client Client) error
257 updateClient(id uuid.ID, change ClientChange) error
258 deleteClient(id uuid.ID) error
259 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
261 addEndpoints(client uuid.ID, endpoint []Endpoint) error
262 removeEndpoint(client, endpoint uuid.ID) error
263 getEndpoint(client, endpoint uuid.ID) (Endpoint, error)
264 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
265 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
266 countEndpoints(client uuid.ID) (int64, error)
267 }
269 func (m *memstore) getClient(id uuid.ID) (Client, error) {
270 m.clientLock.RLock()
271 defer m.clientLock.RUnlock()
272 c, ok := m.clients[id.String()]
273 if !ok {
274 return Client{}, ErrClientNotFound
275 }
276 return c, nil
277 }
279 func (m *memstore) saveClient(client Client) error {
280 m.clientLock.Lock()
281 defer m.clientLock.Unlock()
282 if _, ok := m.clients[client.ID.String()]; ok {
283 return ErrClientAlreadyExists
284 }
285 m.clients[client.ID.String()] = client
286 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
287 return nil
288 }
290 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
291 m.clientLock.Lock()
292 defer m.clientLock.Unlock()
293 c, ok := m.clients[id.String()]
294 if !ok {
295 return ErrClientNotFound
296 }
297 c.ApplyChange(change)
298 m.clients[id.String()] = c
299 return nil
300 }
302 func (m *memstore) deleteClient(id uuid.ID) error {
303 client, err := m.getClient(id)
304 if err != nil {
305 return err
306 }
307 m.clientLock.Lock()
308 defer m.clientLock.Unlock()
309 delete(m.clients, id.String())
310 pos := -1
311 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
312 if item.Equal(id) {
313 pos = p
314 break
315 }
316 }
317 if pos >= 0 {
318 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
319 }
320 return nil
321 }
323 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
324 ids := m.lookupClientsByProfileID(ownerID.String())
325 if len(ids) > num+offset {
326 ids = ids[offset : num+offset]
327 } else if len(ids) > offset {
328 ids = ids[offset:]
329 } else {
330 return []Client{}, nil
331 }
332 clients := []Client{}
333 for _, id := range ids {
334 client, err := m.getClient(id)
335 if err != nil {
336 return []Client{}, err
337 }
338 clients = append(clients, client)
339 }
340 return clients, nil
341 }
343 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error {
344 m.endpointLock.Lock()
345 defer m.endpointLock.Unlock()
346 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...)
347 return nil
348 }
350 func (m *memstore) getEndpoint(client, endpoint uuid.ID) (Endpoint, error) {
351 m.endpointLock.Lock()
352 defer m.endpointLock.Unlock()
353 for _, item := range m.endpoints[client.String()] {
354 if item.ID.Equal(endpoint) {
355 return item, nil
356 }
357 }
358 return Endpoint{}, ErrEndpointNotFound
359 }
361 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
362 m.endpointLock.Lock()
363 defer m.endpointLock.Unlock()
364 pos := -1
365 for p, item := range m.endpoints[client.String()] {
366 if item.ID.Equal(endpoint) {
367 pos = p
368 break
369 }
370 }
371 if pos >= 0 {
372 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
373 }
374 return nil
375 }
377 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
378 m.endpointLock.RLock()
379 defer m.endpointLock.RUnlock()
380 for _, candidate := range m.endpoints[client.String()] {
381 if endpoint == candidate.NormalizedURI {
382 return true, nil
383 }
384 }
385 return false, nil
386 }
388 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
389 m.endpointLock.RLock()
390 defer m.endpointLock.RUnlock()
391 return m.endpoints[client.String()], nil
392 }
394 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
395 m.endpointLock.RLock()
396 defer m.endpointLock.RUnlock()
397 return int64(len(m.endpoints[client.String()])), nil
398 }
400 type newClientReq struct {
401 Name string `json:"name"`
402 Logo string `json:"logo"`
403 Website string `json:"website"`
404 Type string `json:"type"`
405 Endpoints []string `json:"endpoints"`
406 }
408 func RegisterClientHandlers(r *mux.Router, context Context) {
409 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
410 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET")
411 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET")
412 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH")
413 r.Handle("/clients/{id}", wrap(context, RemoveClientHandler)).Methods("DELETE")
414 r.Handle("/clients/{id}/endpoints", wrap(context, AddEndpointsHandler)).Methods("POST")
415 r.Handle("/clients/{client_id}/endpoints/{id}", wrap(context, RemoveEndpointHandler)).Methods("DELETE")
416 r.Handle("/clients/{id}/endpoints", wrap(context, ListEndpointsHandler)).Methods("GET")
417 }
419 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
420 errors := []requestError{}
421 username, password, ok := r.BasicAuth()
422 if !ok {
423 errors = append(errors, requestError{Slug: requestErrAccessDenied})
424 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
425 return
426 }
427 profile, err := authenticate(username, password, c)
428 if err != nil {
429 if isAuthError(err) {
430 errors = append(errors, requestError{Slug: requestErrAccessDenied})
431 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
432 } else {
433 log.Printf("Error authenticating: %#+v\n", err)
434 errors = append(errors, requestError{Slug: requestErrActOfGod})
435 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
436 }
437 return
438 }
439 var req newClientReq
440 decoder := json.NewDecoder(r.Body)
441 err = decoder.Decode(&req)
442 if err != nil {
443 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
444 return
445 }
446 if req.Type == "" {
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"})
450 }
451 if req.Name == "" {
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"})
457 }
458 if len(errors) > 0 {
459 encode(w, r, http.StatusBadRequest, response{Errors: errors})
460 return
461 }
462 client := Client{
463 ID: uuid.NewID(),
464 OwnerID: profile.ID,
465 Name: req.Name,
466 Logo: req.Logo,
467 Website: req.Website,
468 Type: req.Type,
469 }
470 if client.Type == clientTypeConfidential {
471 secret := make([]byte, 32)
472 _, err = rand.Read(secret)
473 if err != nil {
474 log.Printf("Error generating secret: %#+v\n", err)
475 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
476 return
477 }
478 client.Secret = hex.EncodeToString(secret)
479 }
480 err = c.SaveClient(client)
481 if err != nil {
482 if err == ErrClientAlreadyExists {
483 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
484 encode(w, r, http.StatusBadRequest, response{Errors: errors})
485 return
486 }
487 log.Printf("Error saving client: %#+v\n", err)
488 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
489 return
490 }
491 endpoints := []Endpoint{}
492 for pos, u := range req.Endpoints {
493 uri, err := url.Parse(u)
494 if err != nil {
495 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
496 continue
497 }
498 if !uri.IsAbs() {
499 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
500 continue
501 }
502 endpoint := Endpoint{
503 ID: uuid.NewID(),
504 ClientID: client.ID,
505 URI: uri.String(),
506 Added: time.Now(),
507 }
508 endpoints = append(endpoints, endpoint)
509 }
510 err = c.AddEndpoints(client.ID, endpoints)
511 if err != nil {
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}})
515 return
516 }
517 resp := response{
518 Clients: []Client{client},
519 Endpoints: endpoints,
520 Errors: errors,
521 }
522 encode(w, r, http.StatusCreated, resp)
523 }
525 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
526 errors := []requestError{}
527 vars := mux.Vars(r)
528 if vars["id"] == "" {
529 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
530 encode(w, r, http.StatusBadRequest, response{Errors: errors})
531 return
532 }
533 id, err := uuid.Parse(vars["id"])
534 if err != nil {
535 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
536 encode(w, r, http.StatusBadRequest, response{Errors: errors})
537 return
538 }
539 client, err := c.GetClient(id)
540 if err != nil {
541 if err == ErrClientNotFound {
542 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
543 encode(w, r, http.StatusNotFound, response{Errors: errors})
544 return
545 }
546 errors = append(errors, requestError{Slug: requestErrActOfGod})
547 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
548 return
549 }
550 username, password, ok := r.BasicAuth()
551 if !ok {
552 client.Secret = ""
553 } else {
554 profile, err := authenticate(username, password, c)
555 if err != nil {
556 if isAuthError(err) {
557 errors = append(errors, requestError{Slug: requestErrAccessDenied})
558 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
559 } else {
560 errors = append(errors, requestError{Slug: requestErrActOfGod})
561 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
562 }
563 return
564 }
565 if !client.OwnerID.Equal(profile.ID) {
566 client.Secret = ""
567 }
568 }
569 resp := response{
570 Clients: []Client{client},
571 Errors: errors,
572 }
573 encode(w, r, http.StatusOK, resp)
574 }
576 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
577 errors := []requestError{}
578 var err error
579 // BUG(paddy): If ids are provided in query params, retrieve only those clients
580 num := defaultClientResponseSize
581 offset := 0
582 ownerIDStr := r.URL.Query().Get("owner_id")
583 numStr := r.URL.Query().Get("num")
584 offsetStr := r.URL.Query().Get("offset")
585 if numStr != "" {
586 num, err = strconv.Atoi(numStr)
587 if err != nil {
588 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
589 }
590 if num > maxClientResponseSize {
591 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
592 }
593 }
594 if offsetStr != "" {
595 offset, err = strconv.Atoi(offsetStr)
596 if err != nil {
597 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
598 }
599 }
600 if ownerIDStr == "" {
601 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
602 }
603 if len(errors) > 0 {
604 encode(w, r, http.StatusBadRequest, response{Errors: errors})
605 return
606 }
607 ownerID, err := uuid.Parse(ownerIDStr)
608 if err != nil {
609 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
610 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
611 return
612 }
613 clients, err := c.ListClientsByOwner(ownerID, num, offset)
614 if err != nil {
615 errors = append(errors, requestError{Slug: requestErrActOfGod})
616 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
617 return
618 }
619 username, password, ok := r.BasicAuth()
620 if !ok {
621 for pos, client := range clients {
622 client.Secret = ""
623 clients[pos] = client
624 }
625 } else {
626 profile, err := authenticate(username, password, c)
627 if err != nil {
628 if isAuthError(err) {
629 errors = append(errors, requestError{Slug: requestErrAccessDenied})
630 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
631 } else {
632 errors = append(errors, requestError{Slug: requestErrActOfGod})
633 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
634 }
635 return
636 }
637 for pos, client := range clients {
638 if !client.OwnerID.Equal(profile.ID) {
639 client.Secret = ""
640 clients[pos] = client
641 }
642 }
643 }
644 resp := response{
645 Clients: clients,
646 Errors: errors,
647 }
648 encode(w, r, http.StatusOK, resp)
649 }
651 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
652 errors := []requestError{}
653 vars := mux.Vars(r)
654 if _, ok := vars["id"]; !ok {
655 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
656 encode(w, r, http.StatusBadRequest, response{Errors: errors})
657 return
658 }
659 id, err := uuid.Parse(vars["id"])
660 if err != nil {
661 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
662 }
663 username, password, ok := r.BasicAuth()
664 if !ok {
665 errors = append(errors, requestError{Slug: requestErrAccessDenied})
666 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
667 return
668 }
669 profile, err := authenticate(username, password, c)
670 if err != nil {
671 if isAuthError(err) {
672 errors = append(errors, requestError{Slug: requestErrAccessDenied})
673 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
674 } else {
675 errors = append(errors, requestError{Slug: requestErrActOfGod})
676 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
677 }
678 return
679 }
680 var change ClientChange
681 err = decode(r, &change)
682 if err != nil {
683 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
684 encode(w, r, http.StatusBadRequest, response{Errors: errors})
685 return
686 }
687 errs := change.Validate()
688 for _, err := range errs {
689 switch err {
690 case ErrEmptyChange:
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"})
704 default:
705 log.Println("Unrecognised error from client change validation:", err)
706 }
707 }
708 if len(errors) > 0 {
709 encode(w, r, http.StatusBadRequest, response{Errors: errors})
710 return
711 }
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})
716 return
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})
721 return
722 }
723 if !client.OwnerID.Equal(profile.ID) {
724 errors = append(errors, requestError{Slug: requestErrAccessDenied})
725 encode(w, r, http.StatusForbidden, response{Errors: errors})
726 return
727 }
728 if change.Secret != nil && client.Type == clientTypeConfidential {
729 secret := make([]byte, 32)
730 _, err = rand.Read(secret)
731 if err != nil {
732 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
733 return
734 }
735 newSecret := hex.EncodeToString(secret)
736 change.Secret = &newSecret
737 }
738 err = c.UpdateClient(id, change)
739 if err != nil {
740 log.Println("Error updating client:", err)
741 errors = append(errors, requestError{Slug: requestErrActOfGod})
742 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
743 return
744 }
745 client.ApplyChange(change)
746 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
747 return
748 }
750 func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
751 errors := []requestError{}
752 vars := mux.Vars(r)
753 if _, ok := vars["id"]; !ok {
754 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
755 encode(w, r, http.StatusNotFound, response{Errors: errors})
756 return
757 }
758 id, err := uuid.Parse(vars["id"])
759 if err != nil {
760 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
761 }
762 username, password, ok := r.BasicAuth()
763 if !ok {
764 errors = append(errors, requestError{Slug: requestErrAccessDenied})
765 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
766 return
767 }
768 profile, err := authenticate(username, password, c)
769 if err != nil {
770 if isAuthError(err) {
771 errors = append(errors, requestError{Slug: requestErrAccessDenied})
772 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
773 } else {
774 errors = append(errors, requestError{Slug: requestErrActOfGod})
775 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
776 }
777 return
778 }
779 client, err := c.GetClient(id)
780 if err != nil {
781 if err == ErrClientNotFound {
782 errors = append(errors, requestError{Slug: requestErrNotFound})
783 encode(w, r, http.StatusNotFound, response{Errors: errors})
784 return
785 }
786 log.Println("Error retrieving client:", err)
787 errors = append(errors, requestError{Slug: requestErrActOfGod})
788 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
789 return
790 }
791 if !client.OwnerID.Equal(profile.ID) {
792 errors = append(errors, requestError{Slug: requestErrAccessDenied})
793 encode(w, r, http.StatusForbidden, response{Errors: errors})
794 return
795 }
796 err = c.DeleteClient(id)
797 if err != nil {
798 if err == ErrClientNotFound {
799 errors = append(errors, requestError{Slug: requestErrNotFound})
800 encode(w, r, http.StatusNotFound, response{Errors: errors})
801 return
802 }
803 log.Println("Error deleting client:", err)
804 errors = append(errors, requestError{Slug: requestErrActOfGod})
805 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
806 return
807 }
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})
810 return
811 }
813 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
814 type addEndpointReq struct {
815 Endpoints []string `json:"endpoints"`
816 }
817 errors := []requestError{}
818 vars := mux.Vars(r)
819 if vars["id"] == "" {
820 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
821 encode(w, r, http.StatusBadRequest, response{Errors: errors})
822 return
823 }
824 id, err := uuid.Parse(vars["id"])
825 if err != nil {
826 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
827 encode(w, r, http.StatusBadRequest, response{Errors: errors})
828 return
829 }
830 username, password, ok := r.BasicAuth()
831 if !ok {
832 errors = append(errors, requestError{Slug: requestErrAccessDenied})
833 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
834 return
835 }
836 profile, err := authenticate(username, password, c)
837 if err != nil {
838 if isAuthError(err) {
839 errors = append(errors, requestError{Slug: requestErrAccessDenied})
840 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
841 } else {
842 errors = append(errors, requestError{Slug: requestErrActOfGod})
843 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
844 }
845 return
846 }
847 client, err := c.GetClient(id)
848 if err != nil {
849 if err == ErrClientNotFound {
850 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
851 encode(w, r, http.StatusBadRequest, response{Errors: errors})
852 return
853 }
854 errors = append(errors, requestError{Slug: requestErrActOfGod})
855 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
856 return
857 }
858 if !client.OwnerID.Equal(profile.ID) {
859 errors = append(errors, requestError{Slug: requestErrAccessDenied})
860 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
861 return
862 }
863 var req addEndpointReq
864 decoder := json.NewDecoder(r.Body)
865 err = decoder.Decode(&req)
866 if err != nil {
867 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
868 return
869 }
870 if len(req.Endpoints) < 1 {
871 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"})
872 encode(w, r, http.StatusBadRequest, response{Errors: errors})
873 return
874 }
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)})
879 continue
880 } else if !parsed.IsAbs() {
881 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
882 continue
883 }
884 e := Endpoint{
885 ID: uuid.NewID(),
886 ClientID: id,
887 URI: u,
888 Added: time.Now(),
889 }
890 endpoints = append(endpoints, e)
891 }
892 if len(errors) > 0 {
893 encode(w, r, http.StatusBadRequest, response{Errors: errors})
894 return
895 }
896 err = c.AddEndpoints(id, endpoints)
897 if err != nil {
898 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
899 return
900 }
901 resp := response{
902 Errors: errors,
903 Endpoints: endpoints,
904 }
905 encode(w, r, http.StatusCreated, resp)
906 }
908 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
909 errors := []requestError{}
910 vars := mux.Vars(r)
911 clientID, err := uuid.Parse(vars["id"])
912 if err != nil {
913 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
914 encode(w, r, http.StatusBadRequest, response{Errors: errors})
915 return
916 }
917 num := defaultEndpointResponseSize
918 offset := 0
919 numStr := r.URL.Query().Get("num")
920 offsetStr := r.URL.Query().Get("offset")
921 if numStr != "" {
922 num, err = strconv.Atoi(numStr)
923 if err != nil {
924 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
925 }
926 if num > maxEndpointResponseSize {
927 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
928 }
929 }
930 if offsetStr != "" {
931 offset, err = strconv.Atoi(offsetStr)
932 if err != nil {
933 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
934 }
935 }
936 if len(errors) > 0 {
937 encode(w, r, http.StatusBadRequest, response{Errors: errors})
938 return
939 }
940 endpoints, err := c.ListEndpoints(clientID, num, offset)
941 if err != nil {
942 errors = append(errors, requestError{Slug: requestErrActOfGod})
943 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
944 return
945 }
946 resp := response{
947 Endpoints: endpoints,
948 Errors: errors,
949 }
950 encode(w, r, http.StatusOK, resp)
951 }
953 func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) {
954 errors := []requestError{}
955 vars := mux.Vars(r)
956 if vars["client_id"] == "" {
957 errors = append(errors, requestError{Slug: requestErrMissing, Param: "client_id"})
958 encode(w, r, http.StatusBadRequest, response{Errors: errors})
959 return
960 }
961 clientID, err := uuid.Parse(vars["client_id"])
962 if err != nil {
963 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
964 encode(w, r, http.StatusBadRequest, response{Errors: errors})
965 return
966 }
967 if vars["id"] == "" {
968 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
969 encode(w, r, http.StatusBadRequest, response{Errors: errors})
970 return
971 }
972 id, err := uuid.Parse(vars["id"])
973 if err != nil {
974 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
975 encode(w, r, http.StatusBadRequest, response{Errors: errors})
976 return
977 }
978 username, password, ok := r.BasicAuth()
979 if !ok {
980 errors = append(errors, requestError{Slug: requestErrAccessDenied})
981 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
982 return
983 }
984 profile, err := authenticate(username, password, c)
985 if err != nil {
986 if isAuthError(err) {
987 errors = append(errors, requestError{Slug: requestErrAccessDenied})
988 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
989 } else {
990 errors = append(errors, requestError{Slug: requestErrActOfGod})
991 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
992 }
993 return
994 }
995 client, err := c.GetClient(clientID)
996 if err != nil {
997 if err == ErrClientNotFound {
998 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "client_id"})
999 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1000 return
1002 errors = append(errors, requestError{Slug: requestErrActOfGod})
1003 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1004 return
1006 if !client.OwnerID.Equal(profile.ID) {
1007 errors = append(errors, requestError{Slug: requestErrAccessDenied})
1008 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
1009 return
1011 endpoint, err := c.GetEndpoint(clientID, id)
1012 if err != nil {
1013 if err == ErrEndpointNotFound {
1014 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
1015 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1016 return
1018 errors = append(errors, requestError{Slug: requestErrActOfGod})
1019 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1020 return
1022 err = c.RemoveEndpoint(clientID, id)
1023 if err != nil {
1024 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
1025 return
1027 resp := response{
1028 Errors: errors,
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"), " ")
1036 valid = true
1037 return
1040 func clientCredentialsAuditString(r *http.Request) string {
1041 return "client_credentials"