auth
auth/client.go
Make all tests that deal with the store interfaces go through the Context. This is mainly important so that pre- and post- save/retrieval/deletion/whatever transforms can be done without doing them in every single implementation of the store. Change the Endpoint URI property to be a string, not a *url.URL. This makes testing easier, JSON responses cleaner, and is all around just a better strategy. Just because we turn it into a URL every now and then doesn't mean that's how we need to store it. Add JSON tags to the Client type and Endpoint type. Create normalizeURI and normalizeURIString methods to... well, normalize the Endpoint URIs. This makes it so that we can compare them, and forgive some arbitrary user behaviour (like slashes, etc.) Add a NormalizedURI property to the Endpoint type. This is where we store the NormalizedURI, which is what we'll be using when we want to check if an endpoint is valid or not. For the sake of tests and predictability, however, we always want to redirect to the URI, not the NormalizedURI. Add checks to the Client creation API endpoint to give better errors. Now leaving out the Type won't be considered an invalid type, it will be considered a missing parameter. An empty name will be reported as a missing parameter, a name with too few characters will be reported as an insufficient name, and a name with too many characters will be reported as an overflow name. We gather as many of these errors as apply before returning. Check if an Endpoint URI is absolute before adding it as an endpoint, or return an invalid value error if it is not. Always return the errors array when creating a client. We could succeed in creating one or more things and still have errors. We should return anything that's created _as well as_ any errors encountered. Add unit testing for our CreateClientHandler. Fix our oauth2 tests so that if there's an error in the body, it's in the test logs. This should help debugging significantly. Fix our oauth2 tests so that the Profile only requires 1 iteration for its password hashing. This means each time we want to validate a session, it doesn't add a full second to our test runs. This is a big speed improvement for our tests. Add test helper methods for comparing API errors, API responses, and filling in server-generated information in a response that it's impossible to have an expectation around (e.g., IDs) so that we can use our comparison helpers to check if a response is as we expect it. Fix a typo in our Context helpers that was reporting no sessionStore being set _only_ when a sessionStore was set. So yes, the opposite of what we wanted. Oops. This was discovered by passing all our tests through the context. methods instead of operating on the stores themselves.
| paddy@6 | 1 package auth |
| paddy@0 | 2 |
| paddy@0 | 3 import ( |
| paddy@108 | 4 "crypto/rand" |
| paddy@108 | 5 "encoding/hex" |
| paddy@85 | 6 "encoding/json" |
| paddy@31 | 7 "errors" |
| paddy@116 | 8 "log" |
| paddy@85 | 9 "net/http" |
| paddy@41 | 10 "net/url" |
| paddy@115 | 11 "strconv" |
| paddy@41 | 12 "time" |
| paddy@31 | 13 |
| paddy@116 | 14 "github.com/PuerkitoBio/purell" |
| paddy@116 | 15 "github.com/gorilla/mux" |
| paddy@116 | 16 |
| paddy@107 | 17 "code.secondbit.org/uuid.hg" |
| paddy@0 | 18 ) |
| paddy@0 | 19 |
| paddy@31 | 20 var ( |
| paddy@57 | 21 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first. |
| paddy@57 | 22 ErrNoClientStore = errors.New("no clientStore was specified for the Context") |
| paddy@57 | 23 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore. |
| paddy@57 | 24 ErrClientNotFound = errors.New("client not found in clientStore") |
| paddy@57 | 25 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with |
| paddy@57 | 26 // the same ID already exists in the clientStore. |
| paddy@57 | 27 ErrClientAlreadyExists = errors.New("client already exists in clientStore") |
| paddy@41 | 28 |
| paddy@57 | 29 // ErrEmptyChange is returned when a Change has all its properties set to nil. |
| paddy@57 | 30 ErrEmptyChange = errors.New("change must have at least one property set") |
| paddy@57 | 31 // ErrClientNameTooShort is returned when a Client's Name property is too short. |
| paddy@57 | 32 ErrClientNameTooShort = errors.New("client name must be at least 2 characters") |
| paddy@57 | 33 // ErrClientNameTooLong is returned when a Client's Name property is too long. |
| paddy@57 | 34 ErrClientNameTooLong = errors.New("client name must be at most 32 characters") |
| paddy@57 | 35 // ErrClientLogoTooLong is returned when a Client's Logo property is too long. |
| paddy@57 | 36 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters") |
| paddy@57 | 37 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL. |
| paddy@57 | 38 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL") |
| paddy@57 | 39 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long. |
| paddy@49 | 40 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters") |
| paddy@57 | 41 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL. |
| paddy@57 | 42 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL") |
| paddy@116 | 43 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL. |
| paddy@116 | 44 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL") |
| paddy@31 | 45 ) |
| paddy@31 | 46 |
| paddy@115 | 47 const ( |
| paddy@115 | 48 clientTypePublic = "public" |
| paddy@115 | 49 clientTypeConfidential = "confidential" |
| paddy@116 | 50 minClientNameLen = 2 |
| paddy@116 | 51 maxClientNameLen = 24 |
| paddy@115 | 52 ) |
| paddy@115 | 53 |
| paddy@25 | 54 // Client represents a client that grants access |
| paddy@25 | 55 // to the auth server, exchanging grants for tokens, |
| paddy@25 | 56 // and tokens for access. |
| paddy@0 | 57 type Client struct { |
| paddy@116 | 58 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 59 Secret string `json:"secret,omitempty"` |
| paddy@116 | 60 OwnerID uuid.ID `json:"owner_id,omitempty"` |
| paddy@116 | 61 Name string `json:"name,omitempty"` |
| paddy@116 | 62 Logo string `json:"logo,omitempty"` |
| paddy@116 | 63 Website string `json:"website,omitempty"` |
| paddy@116 | 64 Type string `json:"type,omitempty"` |
| paddy@0 | 65 } |
| paddy@0 | 66 |
| paddy@57 | 67 // ApplyChange applies the properties of the passed |
| paddy@57 | 68 // ClientChange to the Client object it is called on. |
| paddy@39 | 69 func (c *Client) ApplyChange(change ClientChange) { |
| paddy@39 | 70 if change.Secret != nil { |
| paddy@39 | 71 c.Secret = *change.Secret |
| paddy@39 | 72 } |
| paddy@39 | 73 if change.OwnerID != nil { |
| paddy@39 | 74 c.OwnerID = change.OwnerID |
| paddy@39 | 75 } |
| paddy@39 | 76 if change.Name != nil { |
| paddy@39 | 77 c.Name = *change.Name |
| paddy@39 | 78 } |
| paddy@39 | 79 if change.Logo != nil { |
| paddy@39 | 80 c.Logo = *change.Logo |
| paddy@39 | 81 } |
| paddy@39 | 82 if change.Website != nil { |
| paddy@39 | 83 c.Website = *change.Website |
| paddy@39 | 84 } |
| paddy@39 | 85 } |
| paddy@39 | 86 |
| paddy@57 | 87 // ClientChange represents a bundle of options for |
| paddy@57 | 88 // updating a Client's mutable data. |
| paddy@31 | 89 type ClientChange struct { |
| paddy@41 | 90 Secret *string |
| paddy@41 | 91 OwnerID uuid.ID |
| paddy@41 | 92 Name *string |
| paddy@41 | 93 Logo *string |
| paddy@41 | 94 Website *string |
| paddy@31 | 95 } |
| paddy@31 | 96 |
| paddy@57 | 97 // Validate checks the ClientChange it is called on |
| paddy@57 | 98 // and asserts its internal validity, or lack thereof. |
| paddy@39 | 99 func (c ClientChange) Validate() error { |
| paddy@42 | 100 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil { |
| paddy@42 | 101 return ErrEmptyChange |
| paddy@42 | 102 } |
| paddy@41 | 103 if c.Name != nil && len(*c.Name) < 2 { |
| paddy@41 | 104 return ErrClientNameTooShort |
| paddy@41 | 105 } |
| paddy@41 | 106 if c.Name != nil && len(*c.Name) > 32 { |
| paddy@41 | 107 return ErrClientNameTooLong |
| paddy@41 | 108 } |
| paddy@42 | 109 if c.Logo != nil && *c.Logo != "" { |
| paddy@42 | 110 if len(*c.Logo) > 1024 { |
| paddy@42 | 111 return ErrClientLogoTooLong |
| paddy@42 | 112 } |
| paddy@42 | 113 u, err := url.Parse(*c.Logo) |
| paddy@42 | 114 if err != nil || !u.IsAbs() { |
| paddy@42 | 115 return ErrClientLogoNotURL |
| paddy@42 | 116 } |
| paddy@41 | 117 } |
| paddy@42 | 118 if c.Website != nil && *c.Website != "" { |
| paddy@42 | 119 if len(*c.Website) > 140 { |
| paddy@42 | 120 return ErrClientWebsiteTooLong |
| paddy@42 | 121 } |
| paddy@42 | 122 u, err := url.Parse(*c.Website) |
| paddy@42 | 123 if err != nil || !u.IsAbs() { |
| paddy@42 | 124 return ErrClientWebsiteNotURL |
| paddy@42 | 125 } |
| paddy@41 | 126 } |
| paddy@39 | 127 return nil |
| paddy@39 | 128 } |
| paddy@39 | 129 |
| paddy@85 | 130 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) { |
| paddy@85 | 131 enc := json.NewEncoder(w) |
| paddy@85 | 132 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 133 if !fromAuthHeader { |
| paddy@85 | 134 if !allowPublic { |
| paddy@85 | 135 w.WriteHeader(http.StatusBadRequest) |
| paddy@85 | 136 renderJSONError(enc, "unauthorized_client") |
| paddy@85 | 137 return nil, false |
| paddy@85 | 138 } |
| paddy@85 | 139 clientIDStr = r.PostFormValue("client_id") |
| paddy@85 | 140 } |
| paddy@85 | 141 clientID, err := uuid.Parse(clientIDStr) |
| paddy@85 | 142 if err != nil { |
| paddy@85 | 143 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 144 if fromAuthHeader { |
| paddy@85 | 145 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 146 } |
| paddy@85 | 147 renderJSONError(enc, "invalid_client") |
| paddy@85 | 148 return nil, false |
| paddy@85 | 149 } |
| paddy@85 | 150 client, err := context.GetClient(clientID) |
| paddy@85 | 151 if err == ErrClientNotFound { |
| paddy@85 | 152 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 153 if fromAuthHeader { |
| paddy@85 | 154 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 155 } |
| paddy@85 | 156 renderJSONError(enc, "invalid_client") |
| paddy@85 | 157 return nil, false |
| paddy@85 | 158 } else if err != nil { |
| paddy@85 | 159 w.WriteHeader(http.StatusInternalServerError) |
| paddy@85 | 160 renderJSONError(enc, "server_error") |
| paddy@85 | 161 return nil, false |
| paddy@85 | 162 } |
| paddy@113 | 163 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret. |
| paddy@85 | 164 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 165 if fromAuthHeader { |
| paddy@85 | 166 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 167 } |
| paddy@85 | 168 renderJSONError(enc, "invalid_client") |
| paddy@85 | 169 return nil, false |
| paddy@85 | 170 } |
| paddy@85 | 171 return clientID, true |
| paddy@85 | 172 } |
| paddy@85 | 173 |
| paddy@57 | 174 // Endpoint represents a single URI that a Client |
| paddy@57 | 175 // controls. Users will be redirected to these URIs |
| paddy@57 | 176 // following successful authorization grants and |
| paddy@57 | 177 // exchanges for access tokens. |
| paddy@41 | 178 type Endpoint struct { |
| paddy@116 | 179 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 180 ClientID uuid.ID `json:"client_id,omitempty"` |
| paddy@116 | 181 URI string `json:"uri,omitempty"` |
| paddy@116 | 182 NormalizedURI string `json:"-"` |
| paddy@116 | 183 Added time.Time `json:"added,omitempty"` |
| paddy@116 | 184 } |
| paddy@116 | 185 |
| paddy@116 | 186 func normalizeURIString(in string) (string, error) { |
| paddy@116 | 187 n, err := purell.NormalizeURLString(in, purell.FlagsUsuallySafeNonGreedy|purell.FlagSortQuery) |
| paddy@116 | 188 if err != nil { |
| paddy@116 | 189 log.Println(err) |
| paddy@116 | 190 return in, ErrEndpointURINotURL |
| paddy@116 | 191 } |
| paddy@116 | 192 return n, nil |
| paddy@116 | 193 } |
| paddy@116 | 194 |
| paddy@116 | 195 func normalizeURI(in *url.URL) string { |
| paddy@116 | 196 return purell.NormalizeURL(in, purell.FlagsUsuallySafeNonGreedy|purell.FlagSortQuery) |
| paddy@41 | 197 } |
| paddy@41 | 198 |
| paddy@41 | 199 type sortedEndpoints []Endpoint |
| paddy@41 | 200 |
| paddy@41 | 201 func (s sortedEndpoints) Len() int { |
| paddy@41 | 202 return len(s) |
| paddy@41 | 203 } |
| paddy@41 | 204 |
| paddy@41 | 205 func (s sortedEndpoints) Less(i, j int) bool { |
| paddy@41 | 206 return s[i].Added.Before(s[j].Added) |
| paddy@41 | 207 } |
| paddy@41 | 208 |
| paddy@41 | 209 func (s sortedEndpoints) Swap(i, j int) { |
| paddy@41 | 210 s[i], s[j] = s[j], s[i] |
| paddy@41 | 211 } |
| paddy@41 | 212 |
| paddy@57 | 213 type clientStore interface { |
| paddy@57 | 214 getClient(id uuid.ID) (Client, error) |
| paddy@57 | 215 saveClient(client Client) error |
| paddy@57 | 216 updateClient(id uuid.ID, change ClientChange) error |
| paddy@57 | 217 deleteClient(id uuid.ID) error |
| paddy@57 | 218 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) |
| paddy@41 | 219 |
| paddy@115 | 220 addEndpoints(client uuid.ID, endpoint []Endpoint) error |
| paddy@57 | 221 removeEndpoint(client, endpoint uuid.ID) error |
| paddy@58 | 222 checkEndpoint(client uuid.ID, endpoint string) (bool, error) |
| paddy@57 | 223 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) |
| paddy@57 | 224 countEndpoints(client uuid.ID) (int64, error) |
| paddy@0 | 225 } |
| paddy@31 | 226 |
| paddy@57 | 227 func (m *memstore) getClient(id uuid.ID) (Client, error) { |
| paddy@31 | 228 m.clientLock.RLock() |
| paddy@31 | 229 defer m.clientLock.RUnlock() |
| paddy@31 | 230 c, ok := m.clients[id.String()] |
| paddy@31 | 231 if !ok { |
| paddy@31 | 232 return Client{}, ErrClientNotFound |
| paddy@31 | 233 } |
| paddy@31 | 234 return c, nil |
| paddy@31 | 235 } |
| paddy@31 | 236 |
| paddy@57 | 237 func (m *memstore) saveClient(client Client) error { |
| paddy@31 | 238 m.clientLock.Lock() |
| paddy@31 | 239 defer m.clientLock.Unlock() |
| paddy@31 | 240 if _, ok := m.clients[client.ID.String()]; ok { |
| paddy@31 | 241 return ErrClientAlreadyExists |
| paddy@31 | 242 } |
| paddy@31 | 243 m.clients[client.ID.String()] = client |
| paddy@31 | 244 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID) |
| paddy@31 | 245 return nil |
| paddy@31 | 246 } |
| paddy@31 | 247 |
| paddy@57 | 248 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error { |
| paddy@39 | 249 m.clientLock.Lock() |
| paddy@39 | 250 defer m.clientLock.Unlock() |
| paddy@39 | 251 c, ok := m.clients[id.String()] |
| paddy@39 | 252 if !ok { |
| paddy@39 | 253 return ErrClientNotFound |
| paddy@39 | 254 } |
| paddy@39 | 255 c.ApplyChange(change) |
| paddy@39 | 256 m.clients[id.String()] = c |
| paddy@31 | 257 return nil |
| paddy@31 | 258 } |
| paddy@31 | 259 |
| paddy@57 | 260 func (m *memstore) deleteClient(id uuid.ID) error { |
| paddy@57 | 261 client, err := m.getClient(id) |
| paddy@31 | 262 if err != nil { |
| paddy@31 | 263 return err |
| paddy@31 | 264 } |
| paddy@31 | 265 m.clientLock.Lock() |
| paddy@31 | 266 defer m.clientLock.Unlock() |
| paddy@31 | 267 delete(m.clients, id.String()) |
| paddy@31 | 268 pos := -1 |
| paddy@31 | 269 for p, item := range m.profileClientLookup[client.OwnerID.String()] { |
| paddy@31 | 270 if item.Equal(id) { |
| paddy@31 | 271 pos = p |
| paddy@31 | 272 break |
| paddy@31 | 273 } |
| paddy@31 | 274 } |
| paddy@31 | 275 if pos >= 0 { |
| paddy@31 | 276 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...) |
| paddy@31 | 277 } |
| paddy@31 | 278 return nil |
| paddy@31 | 279 } |
| paddy@31 | 280 |
| paddy@57 | 281 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) { |
| paddy@33 | 282 ids := m.lookupClientsByProfileID(ownerID.String()) |
| paddy@31 | 283 if len(ids) > num+offset { |
| paddy@31 | 284 ids = ids[offset : num+offset] |
| paddy@31 | 285 } else if len(ids) > offset { |
| paddy@31 | 286 ids = ids[offset:] |
| paddy@31 | 287 } else { |
| paddy@31 | 288 return []Client{}, nil |
| paddy@31 | 289 } |
| paddy@31 | 290 clients := []Client{} |
| paddy@31 | 291 for _, id := range ids { |
| paddy@57 | 292 client, err := m.getClient(id) |
| paddy@31 | 293 if err != nil { |
| paddy@31 | 294 return []Client{}, err |
| paddy@31 | 295 } |
| paddy@31 | 296 clients = append(clients, client) |
| paddy@31 | 297 } |
| paddy@31 | 298 return clients, nil |
| paddy@31 | 299 } |
| paddy@41 | 300 |
| paddy@115 | 301 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error { |
| paddy@41 | 302 m.endpointLock.Lock() |
| paddy@41 | 303 defer m.endpointLock.Unlock() |
| paddy@115 | 304 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...) |
| paddy@41 | 305 return nil |
| paddy@41 | 306 } |
| paddy@41 | 307 |
| paddy@57 | 308 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error { |
| paddy@41 | 309 m.endpointLock.Lock() |
| paddy@41 | 310 defer m.endpointLock.Unlock() |
| paddy@41 | 311 pos := -1 |
| paddy@41 | 312 for p, item := range m.endpoints[client.String()] { |
| paddy@41 | 313 if item.ID.Equal(endpoint) { |
| paddy@41 | 314 pos = p |
| paddy@41 | 315 break |
| paddy@41 | 316 } |
| paddy@41 | 317 } |
| paddy@41 | 318 if pos >= 0 { |
| paddy@41 | 319 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...) |
| paddy@41 | 320 } |
| paddy@41 | 321 return nil |
| paddy@41 | 322 } |
| paddy@41 | 323 |
| paddy@58 | 324 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) { |
| paddy@41 | 325 m.endpointLock.RLock() |
| paddy@41 | 326 defer m.endpointLock.RUnlock() |
| paddy@41 | 327 for _, candidate := range m.endpoints[client.String()] { |
| paddy@116 | 328 if endpoint == candidate.NormalizedURI { |
| paddy@41 | 329 return true, nil |
| paddy@41 | 330 } |
| paddy@41 | 331 } |
| paddy@41 | 332 return false, nil |
| paddy@41 | 333 } |
| paddy@41 | 334 |
| paddy@57 | 335 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) { |
| paddy@41 | 336 m.endpointLock.RLock() |
| paddy@41 | 337 defer m.endpointLock.RUnlock() |
| paddy@41 | 338 return m.endpoints[client.String()], nil |
| paddy@41 | 339 } |
| paddy@54 | 340 |
| paddy@57 | 341 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) { |
| paddy@54 | 342 m.endpointLock.RLock() |
| paddy@54 | 343 defer m.endpointLock.RUnlock() |
| paddy@54 | 344 return int64(len(m.endpoints[client.String()])), nil |
| paddy@54 | 345 } |
| paddy@108 | 346 |
| paddy@108 | 347 type newClientReq struct { |
| paddy@108 | 348 Name string `json:"name"` |
| paddy@108 | 349 Logo string `json:"logo"` |
| paddy@108 | 350 Website string `json:"website"` |
| paddy@108 | 351 Type string `json:"type"` |
| paddy@108 | 352 Endpoints []string `json:"endpoints"` |
| paddy@108 | 353 } |
| paddy@108 | 354 |
| paddy@108 | 355 func RegisterClientHandlers(r *mux.Router, context Context) { |
| paddy@108 | 356 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST") |
| paddy@108 | 357 } |
| paddy@108 | 358 |
| paddy@108 | 359 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@115 | 360 errors := []requestError{} |
| paddy@108 | 361 username, password, ok := r.BasicAuth() |
| paddy@108 | 362 if !ok { |
| paddy@115 | 363 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@115 | 364 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@108 | 365 return |
| paddy@108 | 366 } |
| paddy@108 | 367 profile, err := authenticate(username, password, c) |
| paddy@108 | 368 if err != nil { |
| paddy@115 | 369 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@115 | 370 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@108 | 371 return |
| paddy@108 | 372 } |
| paddy@108 | 373 var req newClientReq |
| paddy@108 | 374 decoder := json.NewDecoder(r.Body) |
| paddy@108 | 375 err = decoder.Decode(&req) |
| paddy@108 | 376 if err != nil { |
| paddy@108 | 377 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@108 | 378 return |
| paddy@108 | 379 } |
| paddy@116 | 380 if req.Type == "" { |
| paddy@116 | 381 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"}) |
| paddy@116 | 382 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential { |
| paddy@115 | 383 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"}) |
| paddy@116 | 384 } |
| paddy@116 | 385 if req.Name == "" { |
| paddy@116 | 386 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"}) |
| paddy@116 | 387 } else if len(req.Name) < minClientNameLen { |
| paddy@116 | 388 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) |
| paddy@116 | 389 } else if len(req.Name) > maxClientNameLen { |
| paddy@116 | 390 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) |
| paddy@116 | 391 } |
| paddy@116 | 392 if len(errors) > 0 { |
| paddy@115 | 393 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@108 | 394 return |
| paddy@108 | 395 } |
| paddy@108 | 396 client := Client{ |
| paddy@108 | 397 ID: uuid.NewID(), |
| paddy@108 | 398 OwnerID: profile.ID, |
| paddy@108 | 399 Name: req.Name, |
| paddy@108 | 400 Logo: req.Logo, |
| paddy@108 | 401 Website: req.Website, |
| paddy@108 | 402 Type: req.Type, |
| paddy@108 | 403 } |
| paddy@115 | 404 if client.Type == clientTypePublic { |
| paddy@115 | 405 secret := make([]byte, 32) |
| paddy@115 | 406 _, err = rand.Read(secret) |
| paddy@115 | 407 if err != nil { |
| paddy@115 | 408 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@115 | 409 return |
| paddy@115 | 410 } |
| paddy@115 | 411 client.Secret = hex.EncodeToString(secret) |
| paddy@115 | 412 } |
| paddy@108 | 413 err = c.SaveClient(client) |
| paddy@108 | 414 if err != nil { |
| paddy@115 | 415 if err == ErrClientAlreadyExists { |
| paddy@115 | 416 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"}) |
| paddy@115 | 417 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@115 | 418 return |
| paddy@115 | 419 } |
| paddy@115 | 420 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@108 | 421 return |
| paddy@108 | 422 } |
| paddy@108 | 423 endpoints := []Endpoint{} |
| paddy@115 | 424 for pos, u := range req.Endpoints { |
| paddy@108 | 425 uri, err := url.Parse(u) |
| paddy@108 | 426 if err != nil { |
| paddy@115 | 427 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@108 | 428 continue |
| paddy@108 | 429 } |
| paddy@116 | 430 if !uri.IsAbs() { |
| paddy@116 | 431 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@116 | 432 continue |
| paddy@116 | 433 } |
| paddy@108 | 434 endpoint := Endpoint{ |
| paddy@108 | 435 ID: uuid.NewID(), |
| paddy@108 | 436 ClientID: client.ID, |
| paddy@116 | 437 URI: uri.String(), |
| paddy@108 | 438 Added: time.Now(), |
| paddy@108 | 439 } |
| paddy@108 | 440 endpoints = append(endpoints, endpoint) |
| paddy@108 | 441 } |
| paddy@115 | 442 err = c.AddEndpoints(client.ID, endpoints) |
| paddy@115 | 443 if err != nil { |
| paddy@115 | 444 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@115 | 445 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}}) |
| paddy@115 | 446 return |
| paddy@115 | 447 } |
| paddy@108 | 448 resp := response{ |
| paddy@108 | 449 Clients: []Client{client}, |
| paddy@108 | 450 Endpoints: endpoints, |
| paddy@116 | 451 Errors: errors, |
| paddy@108 | 452 } |
| paddy@108 | 453 encode(w, r, http.StatusCreated, resp) |
| paddy@108 | 454 } |