auth

Paddy 2015-01-29 Parent:163ce22fa4c9 Child:d103a598548c

133:bc842183181d Browse Files

Add Client updating from the API. Add a handler to update Clients using the API. Add a helper that will decode a request for us based on its Content-Type header. Change the ClientChange.Validate function to return as many errors as possible, as opposed to just the first error it encounters. Update the ClientChange.Validate tests to take advantage of the new signature.

client.go client_test.go request.go

     1.1 --- a/client.go	Wed Jan 28 07:27:32 2015 -0500
     1.2 +++ b/client.go	Thu Jan 29 20:40:55 2015 -0500
     1.3 @@ -111,35 +111,37 @@
     1.4  
     1.5  // Validate checks the ClientChange it is called on
     1.6  // and asserts its internal validity, or lack thereof.
     1.7 -func (c ClientChange) Validate() error {
     1.8 +func (c ClientChange) Validate() []error {
     1.9 +	errors := []error{}
    1.10  	if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
    1.11 -		return ErrEmptyChange
    1.12 +		errors = append(errors, ErrEmptyChange)
    1.13 +		return errors
    1.14  	}
    1.15  	if c.Name != nil && len(*c.Name) < 2 {
    1.16 -		return ErrClientNameTooShort
    1.17 +		errors = append(errors, ErrClientNameTooShort)
    1.18  	}
    1.19  	if c.Name != nil && len(*c.Name) > 32 {
    1.20 -		return ErrClientNameTooLong
    1.21 +		errors = append(errors, ErrClientNameTooLong)
    1.22  	}
    1.23  	if c.Logo != nil && *c.Logo != "" {
    1.24  		if len(*c.Logo) > 1024 {
    1.25 -			return ErrClientLogoTooLong
    1.26 +			errors = append(errors, ErrClientLogoTooLong)
    1.27  		}
    1.28  		u, err := url.Parse(*c.Logo)
    1.29  		if err != nil || !u.IsAbs() {
    1.30 -			return ErrClientLogoNotURL
    1.31 +			errors = append(errors, ErrClientLogoNotURL)
    1.32  		}
    1.33  	}
    1.34  	if c.Website != nil && *c.Website != "" {
    1.35  		if len(*c.Website) > 140 {
    1.36 -			return ErrClientWebsiteTooLong
    1.37 +			errors = append(errors, ErrClientWebsiteTooLong)
    1.38  		}
    1.39  		u, err := url.Parse(*c.Website)
    1.40  		if err != nil || !u.IsAbs() {
    1.41 -			return ErrClientWebsiteNotURL
    1.42 +			errors = append(errors, ErrClientWebsiteNotURL)
    1.43  		}
    1.44  	}
    1.45 -	return nil
    1.46 +	return errors
    1.47  }
    1.48  
    1.49  func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) {
    1.50 @@ -390,7 +392,7 @@
    1.51  	r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
    1.52  	r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET")
    1.53  	r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET")
    1.54 -	// BUG(paddy): We need to implement a handler to update a client.
    1.55 +	r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH")
    1.56  	// BUG(paddy): We need to implement a handler to delete a client. Also, what should that do with the grants and tokens belonging to that client?
    1.57  	// BUG(paddy): We need to implement a handler to add an endpoint to a client.
    1.58  	// BUG(paddy): We need to implement a handler to remove an endpoint from a client.
    1.59 @@ -583,6 +585,83 @@
    1.60  	encode(w, r, http.StatusOK, resp)
    1.61  }
    1.62  
    1.63 +func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
    1.64 +	errors := []requestError{}
    1.65 +	vars := mux.Vars(r)
    1.66 +	if _, ok := vars["id"]; !ok {
    1.67 +		errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
    1.68 +		encode(w, r, http.StatusBadRequest, response{Errors: errors})
    1.69 +		return
    1.70 +	}
    1.71 +	var change ClientChange
    1.72 +	err := decode(r, &change)
    1.73 +	if err != nil {
    1.74 +		errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
    1.75 +		encode(w, r, http.StatusBadRequest, response{Errors: errors})
    1.76 +		return
    1.77 +	}
    1.78 +	errs := change.Validate()
    1.79 +	for _, err := range errs {
    1.80 +		switch err {
    1.81 +		case ErrEmptyChange:
    1.82 +			errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"})
    1.83 +		case ErrClientNameTooShort:
    1.84 +			errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
    1.85 +		case ErrClientNameTooLong:
    1.86 +			errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
    1.87 +		case ErrClientLogoTooLong:
    1.88 +			errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"})
    1.89 +		case ErrClientLogoNotURL:
    1.90 +			errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"})
    1.91 +		case ErrClientWebsiteTooLong:
    1.92 +			errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"})
    1.93 +		case ErrClientWebsiteNotURL:
    1.94 +			errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"})
    1.95 +		default:
    1.96 +			log.Println("Unrecognised error from client change validation:", err)
    1.97 +		}
    1.98 +	}
    1.99 +	id, err := uuid.Parse(vars["id"])
   1.100 +	if err != nil {
   1.101 +		errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
   1.102 +	}
   1.103 +	if len(errors) > 0 {
   1.104 +		encode(w, r, http.StatusBadRequest, response{Errors: errors})
   1.105 +		return
   1.106 +	}
   1.107 +	client, err := c.GetClient(id)
   1.108 +	if err == ErrClientNotFound {
   1.109 +		errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
   1.110 +		encode(w, r, http.StatusNotFound, response{Errors: errors})
   1.111 +		return
   1.112 +	} else if err != nil {
   1.113 +		log.Println("Error retrieving client:", err)
   1.114 +		errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.115 +		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.116 +		return
   1.117 +	}
   1.118 +	if change.Secret != nil && client.Type == clientTypeConfidential {
   1.119 +		secret := make([]byte, 32)
   1.120 +		_, err = rand.Read(secret)
   1.121 +		if err != nil {
   1.122 +			encode(w, r, http.StatusInternalServerError, actOfGodResponse)
   1.123 +			return
   1.124 +		}
   1.125 +		newSecret := hex.EncodeToString(secret)
   1.126 +		change.Secret = &newSecret
   1.127 +	}
   1.128 +	err = c.UpdateClient(id, change)
   1.129 +	if err != nil {
   1.130 +		log.Println("Error updating client:", err)
   1.131 +		errors = append(errors, requestError{Slug: requestErrActOfGod})
   1.132 +		encode(w, r, http.StatusInternalServerError, response{Errors: errors})
   1.133 +		return
   1.134 +	}
   1.135 +	client.ApplyChange(change)
   1.136 +	encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
   1.137 +	return
   1.138 +}
   1.139 +
   1.140  func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
   1.141  	scope = r.PostFormValue("scope")
   1.142  	valid = true
     2.1 --- a/client_test.go	Wed Jan 28 07:27:32 2015 -0500
     2.2 +++ b/client_test.go	Thu Jan 29 20:40:55 2015 -0500
     2.3 @@ -422,49 +422,68 @@
     2.4  func TestClientChangeValidation(t *testing.T) {
     2.5  	t.Parallel()
     2.6  	change := ClientChange{}
     2.7 -	if err := change.Validate(); err != ErrEmptyChange {
     2.8 +	if err := change.Validate(); err[0] != ErrEmptyChange {
     2.9  		t.Errorf("Expected %s to give an error of %s, gave %s", "empty change", ErrEmptyChange, err)
    2.10  	}
    2.11 -	names := map[string]error{
    2.12 -		"a":   ErrClientNameTooShort,
    2.13 -		"ab":  nil,
    2.14 -		"abc": nil,
    2.15 -		"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopq": ErrClientNameTooLong,
    2.16 +	names := map[string][]error{
    2.17 +		"a":   []error{ErrClientNameTooShort},
    2.18 +		"ab":  []error{},
    2.19 +		"abc": []error{},
    2.20 +		"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopq": []error{ErrClientNameTooLong},
    2.21  	}
    2.22  	for name, expectation := range names {
    2.23  		change = ClientChange{Name: &name}
    2.24 -		if err := change.Validate(); err != expectation {
    2.25 -			t.Errorf("Expected %s to give an error of %s, gave %s", name, expectation, err)
    2.26 +		errs := change.Validate()
    2.27 +		if len(errs) != len(expectation) {
    2.28 +			t.Errorf("Expected %s to give %d errors, gave %d", name, len(expectation), len(errs))
    2.29 +			t.Logf("%+v", errs)
    2.30 +		}
    2.31 +		for pos, err := range errs {
    2.32 +			if err != expectation[pos] {
    2.33 +				t.Errorf("Expected %s to give an error of %s in position %d, gave %s", name, expectation[pos], pos, err)
    2.34 +			}
    2.35  		}
    2.36  	}
    2.37  	longPath := ""
    2.38  	for i := 0; i < 1025; i++ {
    2.39  		longPath = fmt.Sprintf("%s%d", longPath, i)
    2.40  	}
    2.41 -	logos := map[string]error{
    2.42 -		"https://www.example.com/" + longPath: ErrClientLogoTooLong,
    2.43 -		"https://www.example.com/ab":          nil,
    2.44 -		"www.example.com/ab":                  ErrClientLogoNotURL,
    2.45 -		"test":                                ErrClientLogoNotURL,
    2.46 -		"":                                    nil,
    2.47 +	logos := map[string][]error{
    2.48 +		"https://www.example.com/" + longPath: []error{ErrClientLogoTooLong},
    2.49 +		"https://www.example.com/ab":          []error{},
    2.50 +		"www.example.com/ab":                  []error{ErrClientLogoNotURL},
    2.51 +		"test":                                []error{ErrClientLogoNotURL},
    2.52 +		"":                                    []error{},
    2.53  	}
    2.54  	for logo, expectation := range logos {
    2.55  		change = ClientChange{Logo: &logo}
    2.56 -		if err := change.Validate(); err != expectation {
    2.57 -			t.Errorf("Expected %s to give an error of %s, gave %s", logo, expectation, err)
    2.58 +		errs := change.Validate()
    2.59 +		if len(errs) != len(expectation) {
    2.60 +			t.Errorf("Expected %s to give %d errors, gave %d", logo, len(expectation), len(errs))
    2.61 +		}
    2.62 +		for pos, err := range errs {
    2.63 +			if err != expectation[pos] {
    2.64 +				t.Errorf("Expected %s to give an error of %s in positiong %d, gave %s", logo, expectation[pos], pos, err)
    2.65 +			}
    2.66  		}
    2.67  	}
    2.68 -	websites := map[string]error{
    2.69 -		"https://www.example.com/" + longPath: ErrClientWebsiteTooLong,
    2.70 -		"https://www.example.com/ab":          nil,
    2.71 -		"www.example.com/ab":                  ErrClientWebsiteNotURL,
    2.72 -		"test":                                ErrClientWebsiteNotURL,
    2.73 -		"":                                    nil,
    2.74 +	websites := map[string][]error{
    2.75 +		"https://www.example.com/" + longPath: []error{ErrClientWebsiteTooLong},
    2.76 +		"https://www.example.com/ab":          []error{},
    2.77 +		"www.example.com/ab":                  []error{ErrClientWebsiteNotURL},
    2.78 +		"test":                                []error{ErrClientWebsiteNotURL},
    2.79 +		"":                                    []error{},
    2.80  	}
    2.81  	for website, expectation := range websites {
    2.82  		change = ClientChange{Website: &website}
    2.83 -		if err := change.Validate(); err != expectation {
    2.84 -			t.Errorf("Expected %s to give an error of %s, gave %s", website, expectation, err)
    2.85 +		errs := change.Validate()
    2.86 +		if len(errs) != len(expectation) {
    2.87 +			t.Errorf("Expected %s to give %d errors, gave %d", website, len(expectation), len(errs))
    2.88 +		}
    2.89 +		for pos, err := range errs {
    2.90 +			if err != expectation[pos] {
    2.91 +				t.Errorf("Expected %s to give an error of %s in position %d, gave %s", website, expectation[pos], pos, err)
    2.92 +			}
    2.93  		}
    2.94  	}
    2.95  }
     3.1 --- a/request.go	Wed Jan 28 07:27:32 2015 -0500
     3.2 +++ b/request.go	Thu Jan 29 20:40:55 2015 -0500
     3.3 @@ -74,6 +74,18 @@
     3.4  	}
     3.5  }
     3.6  
     3.7 +func decode(r *http.Request, target interface{}) error {
     3.8 +	defer r.Body.Close()
     3.9 +	switch r.Header.Get("Content-Type") {
    3.10 +	case "application/json":
    3.11 +		dec := json.NewDecoder(r.Body)
    3.12 +		return dec.Decode(target)
    3.13 +	default:
    3.14 +		dec := json.NewDecoder(r.Body)
    3.15 +		return dec.Decode(target)
    3.16 +	}
    3.17 +}
    3.18 +
    3.19  func wrap(context Context, f func(w http.ResponseWriter, r *http.Request, context Context)) http.Handler {
    3.20  	return negotiate(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    3.21  		f(w, r, context)