auth

Paddy 2015-07-15 Parent:8ecb60d29b0d Child:b7e685839a1b

178:0a2c3d677161 Go to Latest

auth/client.go

Update to use a generic event emitter. Rather can creating a purpose-built event emitter for each and every event we need to emit (I'm looking at you, login verification event) which is _downright silly_, we're now using a generic event publisher that's based on saying "HEY A MODEL UPDATED". This means we need to change all our setup code in authd to use events.NewNSQPublisher or events.NewStdoutPublisher instead of our homegrown solutions. Which also means updating our config to take an events.Publisher instead of our LoginVerificationNotifier (blergh). Our Context also now uses an events.Publisher instead of a LoginVerificationNotifier. Party all around! We also replaced our SendLoginVerification helper method on Context with a SendModelEvent helper method on Context, which is just a light wrapper around events.PublishModelEvent. Of course, all this means we need to update our email_verification listener to listen to the correct channel (based on the model we want updates about) and filter down to a Created action or our new custom action for "the customer wants their verification resent", which I'm OK making a special case and not generic, because c'mon. But we had a subtle change to all our constants, some of which are unofficial constants now. I'm unsure how I feel about this. We also updated our email_verification listener so that we're unmarshalling to a custom loginEvent, which is just an events.Event that overwrites the Data property to be an auth.Login instance. This is to make sure we don't need to wrangle a map[string]interface{}, which is no fun. I'm also OK with special-casing like this, because it's 1) a tiny amount of code, 2) properly utilising composition, and 3) the only way I can think of to cleanly accomplish what I want. I also added a note about GetLogin's deficient handling of logins, namely that it doesn't recognise admins and return Verification codes to them, which would be a useful property for internal tools to take advantage of. Ah well. I updated the Profile and Login implementations so they're now event.Model instances, mainly by just exporting some strings from them through getters that will let us automatically build an Event from them. This lets us use the PublishModelEvent helper. I updated our CreateProfileHandler to properly mangle the login Verification property, and to fire off the ActionCreated events for the new Login and the new Profile. I updated our GetLoginHandler and UpdateLoginHandler to properly mangle the loginVerification property. God that's annoying. :-/ You'll note I didn't start publishing the events.ActionUpdated or events.ActionDeleted events for Profiles or Logins yet, and didn't bother publishing any events for literally any other type. That's because I'm a lazy piece of crap and will end up publishing them when I absolutely have to. Part of that is because if a channel isn't created/being read for a topic, the messages will just stack up in NSQ, and I don't want that. But mostly I'm lazy. Finally, I got to delete the entire profile_verification.go file, because we're no longer special-casing that. Hooray!

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)
271 deleteClientsByOwner(ownerID uuid.ID) error
273 addEndpoints(endpoint []Endpoint) error
274 removeEndpoint(client, endpoint uuid.ID) error
275 getEndpoint(client, endpoint uuid.ID) (Endpoint, error)
276 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
277 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
278 removeEndpointsByClientID(client uuid.ID) error
279 countEndpoints(client uuid.ID) (int64, error)
280 }
282 func (m *memstore) getClient(id uuid.ID) (Client, error) {
283 m.clientLock.RLock()
284 defer m.clientLock.RUnlock()
285 c, ok := m.clients[id.String()]
286 if !ok || c.Deleted {
287 return Client{}, ErrClientNotFound
288 }
289 return c, nil
290 }
292 func (m *memstore) saveClient(client Client) error {
293 m.clientLock.Lock()
294 defer m.clientLock.Unlock()
295 if _, ok := m.clients[client.ID.String()]; ok {
296 return ErrClientAlreadyExists
297 }
298 m.clients[client.ID.String()] = client
299 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
300 return nil
301 }
303 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
304 m.clientLock.Lock()
305 defer m.clientLock.Unlock()
306 c, ok := m.clients[id.String()]
307 if !ok {
308 return ErrClientNotFound
309 }
310 c.ApplyChange(change)
311 m.clients[id.String()] = c
312 return nil
313 }
315 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
316 ids := m.lookupClientsByProfileID(ownerID.String())
317 if len(ids) > num+offset && num > 0 {
318 ids = ids[offset : num+offset]
319 } else if len(ids) > offset {
320 ids = ids[offset:]
321 } else {
322 return []Client{}, nil
323 }
324 clients := []Client{}
325 for _, id := range ids {
326 client, err := m.getClient(id)
327 if err != nil {
328 if err == ErrClientNotFound {
329 continue
330 }
331 return []Client{}, err
332 }
333 clients = append(clients, client)
334 }
335 return clients, nil
336 }
338 func (m *memstore) deleteClientsByOwner(ownerID uuid.ID) error {
339 ids := m.lookupClientsByProfileID(ownerID.String())
340 m.clientLock.Lock()
341 defer m.clientLock.RUnlock()
342 for _, id := range ids {
343 client, ok := m.clients[id.String()]
344 if !ok {
345 continue
346 }
347 client.Deleted = true
348 m.clients[id.String()] = client
349 }
350 return nil
351 }
353 func (m *memstore) addEndpoints(endpoints []Endpoint) error {
354 m.endpointLock.Lock()
355 defer m.endpointLock.Unlock()
356 clients := map[string][]Endpoint{}
357 for _, endpoint := range endpoints {
358 clients[endpoint.ClientID.String()] = append(clients[endpoint.ClientID.String()], endpoint)
359 }
360 for client, e := range clients {
361 m.endpoints[client] = append(m.endpoints[client], e...)
362 }
363 return nil
364 }
366 func (m *memstore) getEndpoint(client, endpoint uuid.ID) (Endpoint, error) {
367 m.endpointLock.Lock()
368 defer m.endpointLock.Unlock()
369 for _, item := range m.endpoints[client.String()] {
370 if item.ID.Equal(endpoint) {
371 return item, nil
372 }
373 }
374 return Endpoint{}, ErrEndpointNotFound
375 }
377 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
378 m.endpointLock.Lock()
379 defer m.endpointLock.Unlock()
380 pos := -1
381 for p, item := range m.endpoints[client.String()] {
382 if item.ID.Equal(endpoint) {
383 pos = p
384 break
385 }
386 }
387 if pos >= 0 {
388 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
389 }
390 return nil
391 }
393 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
394 m.endpointLock.RLock()
395 defer m.endpointLock.RUnlock()
396 for _, candidate := range m.endpoints[client.String()] {
397 if endpoint == candidate.NormalizedURI {
398 return true, nil
399 }
400 }
401 return false, nil
402 }
404 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
405 m.endpointLock.RLock()
406 defer m.endpointLock.RUnlock()
407 return m.endpoints[client.String()], nil
408 }
410 func (m *memstore) removeEndpointsByClientID(client uuid.ID) error {
411 m.endpointLock.Lock()
412 defer m.endpointLock.Unlock()
413 delete(m.endpoints, client.String())
414 return nil
415 }
417 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
418 m.endpointLock.RLock()
419 defer m.endpointLock.RUnlock()
420 return int64(len(m.endpoints[client.String()])), nil
421 }
423 func cleanUpAfterClientDeletion(client uuid.ID, context Context) {
424 err := context.RemoveEndpointsByClientID(client)
425 if err != nil {
426 log.Printf("Error removing endpoints from client %s: %+v\n", client, err)
427 }
428 err = context.DeleteAuthorizationCodesByClientID(client)
429 if err != nil {
430 log.Printf("Error removing auth codes belonging to client %s: %+v\n", client, err)
431 }
432 err = context.RevokeTokensByClientID(client)
433 if err != nil {
434 log.Printf("Error revoking tokens belonging to client %s: %+v\n", client, err)
435 }
436 }
438 type newClientReq struct {
439 Name string `json:"name"`
440 Logo string `json:"logo"`
441 Website string `json:"website"`
442 Type string `json:"type"`
443 Endpoints []string `json:"endpoints"`
444 }
446 func RegisterClientHandlers(r *mux.Router, context Context) {
447 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
448 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET")
449 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET")
450 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH")
451 r.Handle("/clients/{id}", wrap(context, RemoveClientHandler)).Methods("DELETE")
452 r.Handle("/clients/{id}/endpoints", wrap(context, AddEndpointsHandler)).Methods("POST")
453 r.Handle("/clients/{client_id}/endpoints/{id}", wrap(context, RemoveEndpointHandler)).Methods("DELETE")
454 r.Handle("/clients/{id}/endpoints", wrap(context, ListEndpointsHandler)).Methods("GET")
455 }
457 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
458 errors := []RequestError{}
459 username, password, ok := r.BasicAuth()
460 if !ok {
461 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
462 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
463 return
464 }
465 profile, err := authenticate(username, password, c)
466 if err != nil {
467 if isAuthError(err) {
468 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
469 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
470 } else {
471 log.Printf("Error authenticating: %#+v\n", err)
472 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
473 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
474 }
475 return
476 }
477 var req newClientReq
478 decoder := json.NewDecoder(r.Body)
479 err = decoder.Decode(&req)
480 if err != nil {
481 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
482 return
483 }
484 if req.Type == "" {
485 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/type"})
486 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
487 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/type"})
488 }
489 if req.Name == "" {
490 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/name"})
491 } else if len(req.Name) < minClientNameLen {
492 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/name"})
493 } else if len(req.Name) > maxClientNameLen {
494 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/name"})
495 }
496 if len(errors) > 0 {
497 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
498 return
499 }
500 client := Client{
501 ID: uuid.NewID(),
502 OwnerID: profile.ID,
503 Name: req.Name,
504 Logo: req.Logo,
505 Website: req.Website,
506 Type: req.Type,
507 }
508 if client.Type == clientTypeConfidential {
509 secret := make([]byte, 32)
510 _, err = rand.Read(secret)
511 if err != nil {
512 log.Printf("Error generating secret: %#+v\n", err)
513 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
514 return
515 }
516 client.Secret = hex.EncodeToString(secret)
517 }
518 err = c.SaveClient(client)
519 if err != nil {
520 if err == ErrClientAlreadyExists {
521 errors = append(errors, RequestError{Slug: RequestErrConflict, Field: "/id"})
522 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
523 return
524 }
525 log.Printf("Error saving client: %#+v\n", err)
526 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
527 return
528 }
529 endpoints := []Endpoint{}
530 for pos, u := range req.Endpoints {
531 uri, err := url.Parse(u)
532 if err != nil {
533 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
534 continue
535 }
536 if !uri.IsAbs() {
537 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
538 continue
539 }
540 endpoint := Endpoint{
541 ID: uuid.NewID(),
542 ClientID: client.ID,
543 URI: uri.String(),
544 Added: time.Now(),
545 }
546 endpoints = append(endpoints, endpoint)
547 }
548 err = c.AddEndpoints(endpoints)
549 if err != nil {
550 log.Printf("Error adding endpoints: %#+v\n", err)
551 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
552 encode(w, r, http.StatusInternalServerError, Response{Errors: errors, Clients: []Client{client}})
553 return
554 }
555 resp := Response{
556 Clients: []Client{client},
557 Endpoints: endpoints,
558 Errors: errors,
559 }
560 encode(w, r, http.StatusCreated, resp)
561 }
563 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
564 errors := []RequestError{}
565 vars := mux.Vars(r)
566 if vars["id"] == "" {
567 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
568 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
569 return
570 }
571 id, err := uuid.Parse(vars["id"])
572 if err != nil {
573 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
574 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
575 return
576 }
577 client, err := c.GetClient(id)
578 if err != nil {
579 if err == ErrClientNotFound {
580 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
581 encode(w, r, http.StatusNotFound, Response{Errors: errors})
582 return
583 }
584 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
585 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
586 return
587 }
588 username, password, ok := r.BasicAuth()
589 if !ok {
590 client.Secret = ""
591 } else {
592 profile, err := authenticate(username, password, c)
593 if err != nil {
594 if isAuthError(err) {
595 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
596 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
597 } else {
598 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
599 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
600 }
601 return
602 }
603 if !client.OwnerID.Equal(profile.ID) {
604 client.Secret = ""
605 }
606 }
607 resp := Response{
608 Clients: []Client{client},
609 Errors: errors,
610 }
611 encode(w, r, http.StatusOK, resp)
612 }
614 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
615 errors := []RequestError{}
616 var err error
617 // BUG(paddy): If ids are provided in query params, retrieve only those clients
618 num := defaultClientResponseSize
619 offset := 0
620 ownerIDStr := r.URL.Query().Get("owner_id")
621 numStr := r.URL.Query().Get("num")
622 offsetStr := r.URL.Query().Get("offset")
623 if numStr != "" {
624 num, err = strconv.Atoi(numStr)
625 if err != nil {
626 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "num"})
627 }
628 if num > maxClientResponseSize {
629 errors = append(errors, RequestError{Slug: RequestErrOverflow, Param: "num"})
630 }
631 if num < 1 {
632 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Param: "num"})
633 }
634 }
635 if offsetStr != "" {
636 offset, err = strconv.Atoi(offsetStr)
637 if err != nil {
638 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "offset"})
639 }
640 }
641 if ownerIDStr == "" {
642 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "owner_id"})
643 }
644 if len(errors) > 0 {
645 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
646 return
647 }
648 ownerID, err := uuid.Parse(ownerIDStr)
649 if err != nil {
650 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "owner_id"})
651 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
652 return
653 }
654 clients, err := c.ListClientsByOwner(ownerID, num, offset)
655 if err != nil {
656 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
657 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
658 return
659 }
660 username, password, ok := r.BasicAuth()
661 if !ok {
662 for pos, client := range clients {
663 client.Secret = ""
664 clients[pos] = client
665 }
666 } else {
667 profile, err := authenticate(username, password, c)
668 if err != nil {
669 if isAuthError(err) {
670 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
671 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
672 } else {
673 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
674 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
675 }
676 return
677 }
678 for pos, client := range clients {
679 if !client.OwnerID.Equal(profile.ID) {
680 client.Secret = ""
681 clients[pos] = client
682 }
683 }
684 }
685 resp := Response{
686 Clients: clients,
687 Errors: errors,
688 }
689 encode(w, r, http.StatusOK, resp)
690 }
692 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
693 errors := []RequestError{}
694 vars := mux.Vars(r)
695 if _, ok := vars["id"]; !ok {
696 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
697 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
698 return
699 }
700 id, err := uuid.Parse(vars["id"])
701 if err != nil {
702 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
703 }
704 username, password, ok := r.BasicAuth()
705 if !ok {
706 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
707 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
708 return
709 }
710 profile, err := authenticate(username, password, c)
711 if err != nil {
712 if isAuthError(err) {
713 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
714 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
715 } else {
716 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
717 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
718 }
719 return
720 }
721 var change ClientChange
722 err = decode(r, &change)
723 if err != nil {
724 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/"})
725 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
726 return
727 }
728 errs := change.Validate()
729 for _, err := range errs {
730 switch err {
731 case ErrEmptyChange:
732 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
733 case ErrClientNameTooShort:
734 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/name"})
735 case ErrClientNameTooLong:
736 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/name"})
737 case ErrClientLogoTooLong:
738 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/logo"})
739 case ErrClientLogoNotURL:
740 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/logo"})
741 case ErrClientWebsiteTooLong:
742 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/website"})
743 case ErrClientWebsiteNotURL:
744 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/website"})
745 default:
746 log.Println("Unrecognised error from client change validation:", err)
747 }
748 }
749 if len(errors) > 0 {
750 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
751 return
752 }
753 client, err := c.GetClient(id)
754 if err == ErrClientNotFound {
755 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
756 encode(w, r, http.StatusNotFound, Response{Errors: errors})
757 return
758 } else if err != nil {
759 log.Println("Error retrieving client:", err)
760 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
761 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
762 return
763 }
764 if !client.OwnerID.Equal(profile.ID) {
765 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
766 encode(w, r, http.StatusForbidden, Response{Errors: errors})
767 return
768 }
769 if change.Secret != nil && client.Type == clientTypeConfidential {
770 secret := make([]byte, 32)
771 _, err = rand.Read(secret)
772 if err != nil {
773 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
774 return
775 }
776 newSecret := hex.EncodeToString(secret)
777 change.Secret = &newSecret
778 }
779 err = c.UpdateClient(id, change)
780 if err != nil {
781 log.Println("Error updating client:", err)
782 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
783 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
784 return
785 }
786 client.ApplyChange(change)
787 encode(w, r, http.StatusOK, Response{Clients: []Client{client}, Errors: errors})
788 return
789 }
791 func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
792 errors := []RequestError{}
793 vars := mux.Vars(r)
794 if _, ok := vars["id"]; !ok {
795 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
796 encode(w, r, http.StatusNotFound, Response{Errors: errors})
797 return
798 }
799 id, err := uuid.Parse(vars["id"])
800 if err != nil {
801 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
802 }
803 username, password, ok := r.BasicAuth()
804 if !ok {
805 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
806 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
807 return
808 }
809 profile, err := authenticate(username, password, c)
810 if err != nil {
811 if isAuthError(err) {
812 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
813 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
814 } else {
815 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
816 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
817 }
818 return
819 }
820 client, err := c.GetClient(id)
821 if err != nil {
822 if err == ErrClientNotFound {
823 errors = append(errors, RequestError{Slug: RequestErrNotFound})
824 encode(w, r, http.StatusNotFound, Response{Errors: errors})
825 return
826 }
827 log.Println("Error retrieving client:", err)
828 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
829 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
830 return
831 }
832 if !client.OwnerID.Equal(profile.ID) {
833 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
834 encode(w, r, http.StatusForbidden, Response{Errors: errors})
835 return
836 }
837 deleted := true
838 change := ClientChange{Deleted: &deleted}
839 err = c.UpdateClient(id, change)
840 if err != nil {
841 if err == ErrClientNotFound {
842 errors = append(errors, RequestError{Slug: RequestErrNotFound})
843 encode(w, r, http.StatusNotFound, Response{Errors: errors})
844 return
845 }
846 log.Println("Error deleting client:", err)
847 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
848 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
849 return
850 }
851 encode(w, r, http.StatusOK, Response{Errors: errors})
852 go cleanUpAfterClientDeletion(id, c)
853 return
854 }
856 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
857 type addEndpointReq struct {
858 Endpoints []string `json:"endpoints"`
859 }
860 errors := []RequestError{}
861 vars := mux.Vars(r)
862 if vars["id"] == "" {
863 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
864 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
865 return
866 }
867 id, err := uuid.Parse(vars["id"])
868 if err != nil {
869 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
870 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
871 return
872 }
873 username, password, ok := r.BasicAuth()
874 if !ok {
875 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
876 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
877 return
878 }
879 profile, err := authenticate(username, password, c)
880 if err != nil {
881 if isAuthError(err) {
882 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
883 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
884 } else {
885 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
886 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
887 }
888 return
889 }
890 client, err := c.GetClient(id)
891 if err != nil {
892 if err == ErrClientNotFound {
893 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
894 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
895 return
896 }
897 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
898 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
899 return
900 }
901 if !client.OwnerID.Equal(profile.ID) {
902 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
903 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
904 return
905 }
906 var req addEndpointReq
907 decoder := json.NewDecoder(r.Body)
908 err = decoder.Decode(&req)
909 if err != nil {
910 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
911 return
912 }
913 if len(req.Endpoints) < 1 {
914 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/endpoints"})
915 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
916 return
917 }
918 endpoints := []Endpoint{}
919 for pos, u := range req.Endpoints {
920 if parsed, err := url.Parse(u); err != nil {
921 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
922 continue
923 } else if !parsed.IsAbs() {
924 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
925 continue
926 }
927 e := Endpoint{
928 ID: uuid.NewID(),
929 ClientID: id,
930 URI: u,
931 Added: time.Now(),
932 }
933 endpoints = append(endpoints, e)
934 }
935 if len(errors) > 0 {
936 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
937 return
938 }
939 err = c.AddEndpoints(endpoints)
940 if err != nil {
941 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
942 return
943 }
944 resp := Response{
945 Errors: errors,
946 Endpoints: endpoints,
947 }
948 encode(w, r, http.StatusCreated, resp)
949 }
951 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
952 errors := []RequestError{}
953 vars := mux.Vars(r)
954 clientID, err := uuid.Parse(vars["id"])
955 if err != nil {
956 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "client_id"})
957 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
958 return
959 }
960 num := defaultEndpointResponseSize
961 offset := 0
962 numStr := r.URL.Query().Get("num")
963 offsetStr := r.URL.Query().Get("offset")
964 if numStr != "" {
965 num, err = strconv.Atoi(numStr)
966 if err != nil {
967 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "num"})
968 }
969 if num > maxEndpointResponseSize {
970 errors = append(errors, RequestError{Slug: RequestErrOverflow, Param: "num"})
971 }
972 }
973 if offsetStr != "" {
974 offset, err = strconv.Atoi(offsetStr)
975 if err != nil {
976 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "offset"})
977 }
978 }
979 if len(errors) > 0 {
980 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
981 return
982 }
983 endpoints, err := c.ListEndpoints(clientID, num, offset)
984 if err != nil {
985 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
986 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
987 return
988 }
989 resp := Response{
990 Endpoints: endpoints,
991 Errors: errors,
992 }
993 encode(w, r, http.StatusOK, resp)
994 }
996 func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) {
997 errors := []RequestError{}
998 vars := mux.Vars(r)
999 if vars["client_id"] == "" {
1000 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "client_id"})
1001 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
1002 return
1004 clientID, err := uuid.Parse(vars["client_id"])
1005 if err != nil {
1006 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "client_id"})
1007 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
1008 return
1010 if vars["id"] == "" {
1011 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
1012 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
1013 return
1015 id, err := uuid.Parse(vars["id"])
1016 if err != nil {
1017 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
1018 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
1019 return
1021 username, password, ok := r.BasicAuth()
1022 if !ok {
1023 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
1024 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
1025 return
1027 profile, err := authenticate(username, password, c)
1028 if err != nil {
1029 if isAuthError(err) {
1030 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
1031 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
1032 } else {
1033 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
1034 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
1036 return
1038 client, err := c.GetClient(clientID)
1039 if err != nil {
1040 if err == ErrClientNotFound {
1041 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "client_id"})
1042 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
1043 return
1045 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
1046 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
1047 return
1049 if !client.OwnerID.Equal(profile.ID) {
1050 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
1051 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
1052 return
1054 endpoint, err := c.GetEndpoint(clientID, id)
1055 if err != nil {
1056 if err == ErrEndpointNotFound {
1057 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
1058 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
1059 return
1061 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
1062 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
1063 return
1065 err = c.RemoveEndpoint(clientID, id)
1066 if err != nil {
1067 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
1068 return
1070 resp := Response{
1071 Errors: errors,
1072 Endpoints: []Endpoint{endpoint},
1074 encode(w, r, http.StatusCreated, resp)
1077 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) {
1078 scopes = stringsToScopes(strings.Split(r.PostFormValue("scope"), " "))
1079 valid = true
1080 return
1083 func clientCredentialsAuditString(r *http.Request) string {
1084 return "client_credentials"