auth
auth/client.go
Test our GetClientHandler function, add isAuthError helper. Add a helper that identifies whether the error passed to it is an authentication error or is some other type of error. This is useful fo checking whether or not an internal error occurred while authenticating users. Update all instances where we call our authentication helper to make them use the new error helper. All tests continue to pass. Add a new test case for retrieving a client as an unauthenticated user. This clears the client's secret from the response before sending it. Update the GetClientHandler function to return the secret when the owner of the client used Basic Auth in the request. Add a new test case for retrieving a client as an authenticated user, both the owner and a non-owner user. This makes sure the secret is divulged only in the appropriate cases.
| 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@135 | 12 "strings" |
| paddy@41 | 13 "time" |
| paddy@31 | 14 |
| paddy@116 | 15 "github.com/PuerkitoBio/purell" |
| paddy@116 | 16 "github.com/gorilla/mux" |
| paddy@116 | 17 |
| paddy@107 | 18 "code.secondbit.org/uuid.hg" |
| paddy@0 | 19 ) |
| paddy@0 | 20 |
| paddy@121 | 21 func init() { |
| paddy@121 | 22 RegisterGrantType("client_credentials", GrantType{ |
| paddy@121 | 23 Validate: clientCredentialsValidate, |
| paddy@121 | 24 Invalidate: nil, |
| paddy@121 | 25 IssuesRefresh: true, |
| paddy@121 | 26 ReturnToken: RenderJSONToken, |
| paddy@123 | 27 AllowsPublic: false, |
| paddy@124 | 28 AuditString: clientCredentialsAuditString, |
| paddy@121 | 29 }) |
| paddy@121 | 30 } |
| paddy@121 | 31 |
| paddy@31 | 32 var ( |
| paddy@57 | 33 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first. |
| paddy@57 | 34 ErrNoClientStore = errors.New("no clientStore was specified for the Context") |
| paddy@57 | 35 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore. |
| paddy@57 | 36 ErrClientNotFound = errors.New("client not found in clientStore") |
| paddy@57 | 37 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with |
| paddy@57 | 38 // the same ID already exists in the clientStore. |
| paddy@57 | 39 ErrClientAlreadyExists = errors.New("client already exists in clientStore") |
| paddy@41 | 40 |
| paddy@57 | 41 // ErrEmptyChange is returned when a Change has all its properties set to nil. |
| paddy@57 | 42 ErrEmptyChange = errors.New("change must have at least one property set") |
| paddy@57 | 43 // ErrClientNameTooShort is returned when a Client's Name property is too short. |
| paddy@57 | 44 ErrClientNameTooShort = errors.New("client name must be at least 2 characters") |
| paddy@57 | 45 // ErrClientNameTooLong is returned when a Client's Name property is too long. |
| paddy@57 | 46 ErrClientNameTooLong = errors.New("client name must be at most 32 characters") |
| paddy@57 | 47 // ErrClientLogoTooLong is returned when a Client's Logo property is too long. |
| paddy@57 | 48 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters") |
| paddy@57 | 49 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL. |
| paddy@57 | 50 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL") |
| paddy@57 | 51 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long. |
| paddy@49 | 52 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters") |
| paddy@57 | 53 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL. |
| paddy@57 | 54 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL") |
| paddy@116 | 55 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL. |
| paddy@116 | 56 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL") |
| paddy@31 | 57 ) |
| paddy@31 | 58 |
| paddy@115 | 59 const ( |
| paddy@138 | 60 clientTypePublic = "public" |
| paddy@138 | 61 clientTypeConfidential = "confidential" |
| paddy@138 | 62 minClientNameLen = 2 |
| paddy@138 | 63 maxClientNameLen = 24 |
| paddy@138 | 64 defaultClientResponseSize = 20 |
| paddy@138 | 65 maxClientResponseSize = 50 |
| paddy@138 | 66 defaultEndpointResponseSize = 20 |
| paddy@138 | 67 maxEndpointResponseSize = 50 |
| paddy@130 | 68 |
| paddy@130 | 69 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery |
| paddy@115 | 70 ) |
| paddy@115 | 71 |
| paddy@25 | 72 // Client represents a client that grants access |
| paddy@25 | 73 // to the auth server, exchanging grants for tokens, |
| paddy@25 | 74 // and tokens for access. |
| paddy@0 | 75 type Client struct { |
| paddy@116 | 76 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 77 Secret string `json:"secret,omitempty"` |
| paddy@116 | 78 OwnerID uuid.ID `json:"owner_id,omitempty"` |
| paddy@116 | 79 Name string `json:"name,omitempty"` |
| paddy@116 | 80 Logo string `json:"logo,omitempty"` |
| paddy@116 | 81 Website string `json:"website,omitempty"` |
| paddy@116 | 82 Type string `json:"type,omitempty"` |
| paddy@0 | 83 } |
| paddy@0 | 84 |
| paddy@57 | 85 // ApplyChange applies the properties of the passed |
| paddy@57 | 86 // ClientChange to the Client object it is called on. |
| paddy@39 | 87 func (c *Client) ApplyChange(change ClientChange) { |
| paddy@39 | 88 if change.Secret != nil { |
| paddy@39 | 89 c.Secret = *change.Secret |
| paddy@39 | 90 } |
| paddy@39 | 91 if change.OwnerID != nil { |
| paddy@39 | 92 c.OwnerID = change.OwnerID |
| paddy@39 | 93 } |
| paddy@39 | 94 if change.Name != nil { |
| paddy@39 | 95 c.Name = *change.Name |
| paddy@39 | 96 } |
| paddy@39 | 97 if change.Logo != nil { |
| paddy@39 | 98 c.Logo = *change.Logo |
| paddy@39 | 99 } |
| paddy@39 | 100 if change.Website != nil { |
| paddy@39 | 101 c.Website = *change.Website |
| paddy@39 | 102 } |
| paddy@39 | 103 } |
| paddy@39 | 104 |
| paddy@57 | 105 // ClientChange represents a bundle of options for |
| paddy@57 | 106 // updating a Client's mutable data. |
| paddy@31 | 107 type ClientChange struct { |
| paddy@41 | 108 Secret *string |
| paddy@41 | 109 OwnerID uuid.ID |
| paddy@41 | 110 Name *string |
| paddy@41 | 111 Logo *string |
| paddy@41 | 112 Website *string |
| paddy@31 | 113 } |
| paddy@31 | 114 |
| paddy@57 | 115 // Validate checks the ClientChange it is called on |
| paddy@57 | 116 // and asserts its internal validity, or lack thereof. |
| paddy@133 | 117 func (c ClientChange) Validate() []error { |
| paddy@133 | 118 errors := []error{} |
| paddy@42 | 119 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil { |
| paddy@133 | 120 errors = append(errors, ErrEmptyChange) |
| paddy@133 | 121 return errors |
| paddy@42 | 122 } |
| paddy@41 | 123 if c.Name != nil && len(*c.Name) < 2 { |
| paddy@133 | 124 errors = append(errors, ErrClientNameTooShort) |
| paddy@41 | 125 } |
| paddy@41 | 126 if c.Name != nil && len(*c.Name) > 32 { |
| paddy@133 | 127 errors = append(errors, ErrClientNameTooLong) |
| paddy@41 | 128 } |
| paddy@42 | 129 if c.Logo != nil && *c.Logo != "" { |
| paddy@42 | 130 if len(*c.Logo) > 1024 { |
| paddy@133 | 131 errors = append(errors, ErrClientLogoTooLong) |
| paddy@42 | 132 } |
| paddy@42 | 133 u, err := url.Parse(*c.Logo) |
| paddy@42 | 134 if err != nil || !u.IsAbs() { |
| paddy@133 | 135 errors = append(errors, ErrClientLogoNotURL) |
| paddy@42 | 136 } |
| paddy@41 | 137 } |
| paddy@42 | 138 if c.Website != nil && *c.Website != "" { |
| paddy@42 | 139 if len(*c.Website) > 140 { |
| paddy@133 | 140 errors = append(errors, ErrClientWebsiteTooLong) |
| paddy@42 | 141 } |
| paddy@42 | 142 u, err := url.Parse(*c.Website) |
| paddy@42 | 143 if err != nil || !u.IsAbs() { |
| paddy@133 | 144 errors = append(errors, ErrClientWebsiteNotURL) |
| paddy@42 | 145 } |
| paddy@41 | 146 } |
| paddy@133 | 147 return errors |
| paddy@39 | 148 } |
| paddy@39 | 149 |
| paddy@123 | 150 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) { |
| paddy@85 | 151 enc := json.NewEncoder(w) |
| paddy@85 | 152 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 153 if !fromAuthHeader { |
| paddy@85 | 154 clientIDStr = r.PostFormValue("client_id") |
| paddy@85 | 155 } |
| paddy@123 | 156 if clientIDStr == "" { |
| paddy@85 | 157 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 158 if fromAuthHeader { |
| paddy@85 | 159 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 160 } |
| paddy@85 | 161 renderJSONError(enc, "invalid_client") |
| paddy@123 | 162 return nil, "", false |
| paddy@123 | 163 } |
| paddy@129 | 164 if !allowPublic && !fromAuthHeader { |
| paddy@129 | 165 w.WriteHeader(http.StatusBadRequest) |
| paddy@129 | 166 renderJSONError(enc, "unauthorized_client") |
| paddy@129 | 167 return nil, "", false |
| paddy@129 | 168 } |
| paddy@123 | 169 clientID, err := uuid.Parse(clientIDStr) |
| paddy@123 | 170 if err != nil { |
| paddy@123 | 171 log.Println("Error decoding client ID:", err) |
| paddy@123 | 172 w.WriteHeader(http.StatusUnauthorized) |
| paddy@123 | 173 if fromAuthHeader { |
| paddy@123 | 174 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@123 | 175 } |
| paddy@123 | 176 renderJSONError(enc, "invalid_client") |
| paddy@123 | 177 return nil, "", false |
| paddy@123 | 178 } |
| paddy@123 | 179 return clientID, clientSecret, true |
| paddy@123 | 180 } |
| paddy@123 | 181 |
| paddy@123 | 182 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) { |
| paddy@123 | 183 enc := json.NewEncoder(w) |
| paddy@123 | 184 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic) |
| paddy@123 | 185 if !ok { |
| paddy@85 | 186 return nil, false |
| paddy@85 | 187 } |
| paddy@123 | 188 _, _, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 189 client, err := context.GetClient(clientID) |
| paddy@85 | 190 if err == ErrClientNotFound { |
| paddy@85 | 191 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 192 if fromAuthHeader { |
| paddy@85 | 193 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 194 } |
| paddy@85 | 195 renderJSONError(enc, "invalid_client") |
| paddy@85 | 196 return nil, false |
| paddy@85 | 197 } else if err != nil { |
| paddy@85 | 198 w.WriteHeader(http.StatusInternalServerError) |
| paddy@85 | 199 renderJSONError(enc, "server_error") |
| paddy@85 | 200 return nil, false |
| paddy@85 | 201 } |
| paddy@113 | 202 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret. |
| paddy@85 | 203 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 204 if fromAuthHeader { |
| paddy@85 | 205 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 206 } |
| paddy@85 | 207 renderJSONError(enc, "invalid_client") |
| paddy@85 | 208 return nil, false |
| paddy@85 | 209 } |
| paddy@85 | 210 return clientID, true |
| paddy@85 | 211 } |
| paddy@85 | 212 |
| paddy@57 | 213 // Endpoint represents a single URI that a Client |
| paddy@57 | 214 // controls. Users will be redirected to these URIs |
| paddy@57 | 215 // following successful authorization grants and |
| paddy@57 | 216 // exchanges for access tokens. |
| paddy@41 | 217 type Endpoint struct { |
| paddy@116 | 218 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 219 ClientID uuid.ID `json:"client_id,omitempty"` |
| paddy@116 | 220 URI string `json:"uri,omitempty"` |
| paddy@116 | 221 NormalizedURI string `json:"-"` |
| paddy@116 | 222 Added time.Time `json:"added,omitempty"` |
| paddy@116 | 223 } |
| paddy@116 | 224 |
| paddy@116 | 225 func normalizeURIString(in string) (string, error) { |
| paddy@130 | 226 n, err := purell.NormalizeURLString(in, normalizeFlags) |
| paddy@116 | 227 if err != nil { |
| paddy@116 | 228 log.Println(err) |
| paddy@116 | 229 return in, ErrEndpointURINotURL |
| paddy@116 | 230 } |
| paddy@116 | 231 return n, nil |
| paddy@116 | 232 } |
| paddy@116 | 233 |
| paddy@116 | 234 func normalizeURI(in *url.URL) string { |
| paddy@130 | 235 return purell.NormalizeURL(in, normalizeFlags) |
| paddy@41 | 236 } |
| paddy@41 | 237 |
| paddy@41 | 238 type sortedEndpoints []Endpoint |
| paddy@41 | 239 |
| paddy@41 | 240 func (s sortedEndpoints) Len() int { |
| paddy@41 | 241 return len(s) |
| paddy@41 | 242 } |
| paddy@41 | 243 |
| paddy@41 | 244 func (s sortedEndpoints) Less(i, j int) bool { |
| paddy@41 | 245 return s[i].Added.Before(s[j].Added) |
| paddy@41 | 246 } |
| paddy@41 | 247 |
| paddy@41 | 248 func (s sortedEndpoints) Swap(i, j int) { |
| paddy@41 | 249 s[i], s[j] = s[j], s[i] |
| paddy@41 | 250 } |
| paddy@41 | 251 |
| paddy@57 | 252 type clientStore interface { |
| paddy@57 | 253 getClient(id uuid.ID) (Client, error) |
| paddy@57 | 254 saveClient(client Client) error |
| paddy@57 | 255 updateClient(id uuid.ID, change ClientChange) error |
| paddy@57 | 256 deleteClient(id uuid.ID) error |
| paddy@57 | 257 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) |
| paddy@41 | 258 |
| paddy@115 | 259 addEndpoints(client uuid.ID, endpoint []Endpoint) error |
| paddy@57 | 260 removeEndpoint(client, endpoint uuid.ID) error |
| paddy@58 | 261 checkEndpoint(client uuid.ID, endpoint string) (bool, error) |
| paddy@57 | 262 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) |
| paddy@57 | 263 countEndpoints(client uuid.ID) (int64, error) |
| paddy@0 | 264 } |
| paddy@31 | 265 |
| paddy@57 | 266 func (m *memstore) getClient(id uuid.ID) (Client, error) { |
| paddy@31 | 267 m.clientLock.RLock() |
| paddy@31 | 268 defer m.clientLock.RUnlock() |
| paddy@31 | 269 c, ok := m.clients[id.String()] |
| paddy@31 | 270 if !ok { |
| paddy@31 | 271 return Client{}, ErrClientNotFound |
| paddy@31 | 272 } |
| paddy@31 | 273 return c, nil |
| paddy@31 | 274 } |
| paddy@31 | 275 |
| paddy@57 | 276 func (m *memstore) saveClient(client Client) error { |
| paddy@31 | 277 m.clientLock.Lock() |
| paddy@31 | 278 defer m.clientLock.Unlock() |
| paddy@31 | 279 if _, ok := m.clients[client.ID.String()]; ok { |
| paddy@31 | 280 return ErrClientAlreadyExists |
| paddy@31 | 281 } |
| paddy@31 | 282 m.clients[client.ID.String()] = client |
| paddy@31 | 283 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID) |
| paddy@31 | 284 return nil |
| paddy@31 | 285 } |
| paddy@31 | 286 |
| paddy@57 | 287 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error { |
| paddy@39 | 288 m.clientLock.Lock() |
| paddy@39 | 289 defer m.clientLock.Unlock() |
| paddy@39 | 290 c, ok := m.clients[id.String()] |
| paddy@39 | 291 if !ok { |
| paddy@39 | 292 return ErrClientNotFound |
| paddy@39 | 293 } |
| paddy@39 | 294 c.ApplyChange(change) |
| paddy@39 | 295 m.clients[id.String()] = c |
| paddy@31 | 296 return nil |
| paddy@31 | 297 } |
| paddy@31 | 298 |
| paddy@57 | 299 func (m *memstore) deleteClient(id uuid.ID) error { |
| paddy@57 | 300 client, err := m.getClient(id) |
| paddy@31 | 301 if err != nil { |
| paddy@31 | 302 return err |
| paddy@31 | 303 } |
| paddy@31 | 304 m.clientLock.Lock() |
| paddy@31 | 305 defer m.clientLock.Unlock() |
| paddy@31 | 306 delete(m.clients, id.String()) |
| paddy@31 | 307 pos := -1 |
| paddy@31 | 308 for p, item := range m.profileClientLookup[client.OwnerID.String()] { |
| paddy@31 | 309 if item.Equal(id) { |
| paddy@31 | 310 pos = p |
| paddy@31 | 311 break |
| paddy@31 | 312 } |
| paddy@31 | 313 } |
| paddy@31 | 314 if pos >= 0 { |
| paddy@31 | 315 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...) |
| paddy@31 | 316 } |
| paddy@31 | 317 return nil |
| paddy@31 | 318 } |
| paddy@31 | 319 |
| paddy@57 | 320 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) { |
| paddy@33 | 321 ids := m.lookupClientsByProfileID(ownerID.String()) |
| paddy@31 | 322 if len(ids) > num+offset { |
| paddy@31 | 323 ids = ids[offset : num+offset] |
| paddy@31 | 324 } else if len(ids) > offset { |
| paddy@31 | 325 ids = ids[offset:] |
| paddy@31 | 326 } else { |
| paddy@31 | 327 return []Client{}, nil |
| paddy@31 | 328 } |
| paddy@31 | 329 clients := []Client{} |
| paddy@31 | 330 for _, id := range ids { |
| paddy@57 | 331 client, err := m.getClient(id) |
| paddy@31 | 332 if err != nil { |
| paddy@31 | 333 return []Client{}, err |
| paddy@31 | 334 } |
| paddy@31 | 335 clients = append(clients, client) |
| paddy@31 | 336 } |
| paddy@31 | 337 return clients, nil |
| paddy@31 | 338 } |
| paddy@41 | 339 |
| paddy@115 | 340 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error { |
| paddy@41 | 341 m.endpointLock.Lock() |
| paddy@41 | 342 defer m.endpointLock.Unlock() |
| paddy@115 | 343 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...) |
| paddy@41 | 344 return nil |
| paddy@41 | 345 } |
| paddy@41 | 346 |
| paddy@57 | 347 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error { |
| paddy@41 | 348 m.endpointLock.Lock() |
| paddy@41 | 349 defer m.endpointLock.Unlock() |
| paddy@41 | 350 pos := -1 |
| paddy@41 | 351 for p, item := range m.endpoints[client.String()] { |
| paddy@41 | 352 if item.ID.Equal(endpoint) { |
| paddy@41 | 353 pos = p |
| paddy@41 | 354 break |
| paddy@41 | 355 } |
| paddy@41 | 356 } |
| paddy@41 | 357 if pos >= 0 { |
| paddy@41 | 358 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...) |
| paddy@41 | 359 } |
| paddy@41 | 360 return nil |
| paddy@41 | 361 } |
| paddy@41 | 362 |
| paddy@58 | 363 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) { |
| paddy@41 | 364 m.endpointLock.RLock() |
| paddy@41 | 365 defer m.endpointLock.RUnlock() |
| paddy@41 | 366 for _, candidate := range m.endpoints[client.String()] { |
| paddy@116 | 367 if endpoint == candidate.NormalizedURI { |
| paddy@41 | 368 return true, nil |
| paddy@41 | 369 } |
| paddy@41 | 370 } |
| paddy@41 | 371 return false, nil |
| paddy@41 | 372 } |
| paddy@41 | 373 |
| paddy@57 | 374 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) { |
| paddy@41 | 375 m.endpointLock.RLock() |
| paddy@41 | 376 defer m.endpointLock.RUnlock() |
| paddy@41 | 377 return m.endpoints[client.String()], nil |
| paddy@41 | 378 } |
| paddy@54 | 379 |
| paddy@57 | 380 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) { |
| paddy@54 | 381 m.endpointLock.RLock() |
| paddy@54 | 382 defer m.endpointLock.RUnlock() |
| paddy@54 | 383 return int64(len(m.endpoints[client.String()])), nil |
| paddy@54 | 384 } |
| paddy@108 | 385 |
| paddy@108 | 386 type newClientReq struct { |
| paddy@108 | 387 Name string `json:"name"` |
| paddy@108 | 388 Logo string `json:"logo"` |
| paddy@108 | 389 Website string `json:"website"` |
| paddy@108 | 390 Type string `json:"type"` |
| paddy@108 | 391 Endpoints []string `json:"endpoints"` |
| paddy@108 | 392 } |
| paddy@108 | 393 |
| paddy@108 | 394 func RegisterClientHandlers(r *mux.Router, context Context) { |
| paddy@108 | 395 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST") |
| paddy@131 | 396 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET") |
| paddy@131 | 397 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET") |
| paddy@133 | 398 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH") |
| paddy@128 | 399 // 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? |
| paddy@137 | 400 r.Handle("/clients/{id}/endpoints", wrap(context, AddEndpointsHandler)).Methods("POST") |
| paddy@128 | 401 // BUG(paddy): We need to implement a handler to remove an endpoint from a client. |
| paddy@138 | 402 r.Handle("/clients/{id}/endpoints", wrap(context, ListEndpointsHandler)).Methods("GET") |
| paddy@108 | 403 } |
| paddy@108 | 404 |
| paddy@108 | 405 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@115 | 406 errors := []requestError{} |
| paddy@108 | 407 username, password, ok := r.BasicAuth() |
| paddy@108 | 408 if !ok { |
| paddy@115 | 409 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@115 | 410 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@108 | 411 return |
| paddy@108 | 412 } |
| paddy@108 | 413 profile, err := authenticate(username, password, c) |
| paddy@108 | 414 if err != nil { |
| paddy@139 | 415 if isAuthError(err) { |
| paddy@139 | 416 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@139 | 417 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@139 | 418 } else { |
| paddy@139 | 419 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@139 | 420 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@139 | 421 } |
| paddy@108 | 422 return |
| paddy@108 | 423 } |
| paddy@108 | 424 var req newClientReq |
| paddy@108 | 425 decoder := json.NewDecoder(r.Body) |
| paddy@108 | 426 err = decoder.Decode(&req) |
| paddy@108 | 427 if err != nil { |
| paddy@108 | 428 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@108 | 429 return |
| paddy@108 | 430 } |
| paddy@116 | 431 if req.Type == "" { |
| paddy@116 | 432 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"}) |
| paddy@116 | 433 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential { |
| paddy@115 | 434 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"}) |
| paddy@116 | 435 } |
| paddy@116 | 436 if req.Name == "" { |
| paddy@116 | 437 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"}) |
| paddy@116 | 438 } else if len(req.Name) < minClientNameLen { |
| paddy@116 | 439 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) |
| paddy@116 | 440 } else if len(req.Name) > maxClientNameLen { |
| paddy@116 | 441 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) |
| paddy@116 | 442 } |
| paddy@116 | 443 if len(errors) > 0 { |
| paddy@115 | 444 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@108 | 445 return |
| paddy@108 | 446 } |
| paddy@108 | 447 client := Client{ |
| paddy@108 | 448 ID: uuid.NewID(), |
| paddy@108 | 449 OwnerID: profile.ID, |
| paddy@108 | 450 Name: req.Name, |
| paddy@108 | 451 Logo: req.Logo, |
| paddy@108 | 452 Website: req.Website, |
| paddy@108 | 453 Type: req.Type, |
| paddy@108 | 454 } |
| paddy@118 | 455 if client.Type == clientTypeConfidential { |
| paddy@115 | 456 secret := make([]byte, 32) |
| paddy@115 | 457 _, err = rand.Read(secret) |
| paddy@115 | 458 if err != nil { |
| paddy@115 | 459 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@115 | 460 return |
| paddy@115 | 461 } |
| paddy@115 | 462 client.Secret = hex.EncodeToString(secret) |
| paddy@115 | 463 } |
| paddy@108 | 464 err = c.SaveClient(client) |
| paddy@108 | 465 if err != nil { |
| paddy@115 | 466 if err == ErrClientAlreadyExists { |
| paddy@115 | 467 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"}) |
| paddy@115 | 468 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@115 | 469 return |
| paddy@115 | 470 } |
| paddy@115 | 471 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@108 | 472 return |
| paddy@108 | 473 } |
| paddy@108 | 474 endpoints := []Endpoint{} |
| paddy@115 | 475 for pos, u := range req.Endpoints { |
| paddy@108 | 476 uri, err := url.Parse(u) |
| paddy@108 | 477 if err != nil { |
| paddy@115 | 478 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@108 | 479 continue |
| paddy@108 | 480 } |
| paddy@116 | 481 if !uri.IsAbs() { |
| paddy@116 | 482 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@116 | 483 continue |
| paddy@116 | 484 } |
| paddy@108 | 485 endpoint := Endpoint{ |
| paddy@108 | 486 ID: uuid.NewID(), |
| paddy@108 | 487 ClientID: client.ID, |
| paddy@116 | 488 URI: uri.String(), |
| paddy@108 | 489 Added: time.Now(), |
| paddy@108 | 490 } |
| paddy@108 | 491 endpoints = append(endpoints, endpoint) |
| paddy@108 | 492 } |
| paddy@115 | 493 err = c.AddEndpoints(client.ID, endpoints) |
| paddy@115 | 494 if err != nil { |
| paddy@115 | 495 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@115 | 496 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}}) |
| paddy@115 | 497 return |
| paddy@115 | 498 } |
| paddy@108 | 499 resp := response{ |
| paddy@108 | 500 Clients: []Client{client}, |
| paddy@108 | 501 Endpoints: endpoints, |
| paddy@116 | 502 Errors: errors, |
| paddy@108 | 503 } |
| paddy@108 | 504 encode(w, r, http.StatusCreated, resp) |
| paddy@108 | 505 } |
| paddy@121 | 506 |
| paddy@131 | 507 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@131 | 508 errors := []requestError{} |
| paddy@131 | 509 vars := mux.Vars(r) |
| paddy@131 | 510 if vars["id"] == "" { |
| paddy@131 | 511 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@131 | 512 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 513 return |
| paddy@131 | 514 } |
| paddy@131 | 515 id, err := uuid.Parse(vars["id"]) |
| paddy@131 | 516 if err != nil { |
| paddy@131 | 517 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@131 | 518 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 519 return |
| paddy@131 | 520 } |
| paddy@131 | 521 client, err := c.GetClient(id) |
| paddy@131 | 522 if err != nil { |
| paddy@131 | 523 if err == ErrClientNotFound { |
| paddy@131 | 524 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@139 | 525 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@131 | 526 return |
| paddy@131 | 527 } |
| paddy@131 | 528 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@131 | 529 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 530 return |
| paddy@131 | 531 } |
| paddy@139 | 532 username, password, ok := r.BasicAuth() |
| paddy@139 | 533 if !ok { |
| paddy@139 | 534 client.Secret = "" |
| paddy@139 | 535 } else { |
| paddy@139 | 536 profile, err := authenticate(username, password, c) |
| paddy@139 | 537 if err != nil { |
| paddy@139 | 538 if isAuthError(err) { |
| paddy@139 | 539 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@139 | 540 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@139 | 541 } else { |
| paddy@139 | 542 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@139 | 543 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@139 | 544 } |
| paddy@139 | 545 return |
| paddy@139 | 546 } |
| paddy@139 | 547 if !client.OwnerID.Equal(profile.ID) { |
| paddy@139 | 548 client.Secret = "" |
| paddy@139 | 549 } |
| paddy@139 | 550 } |
| paddy@131 | 551 resp := response{ |
| paddy@131 | 552 Clients: []Client{client}, |
| paddy@131 | 553 Errors: errors, |
| paddy@131 | 554 } |
| paddy@131 | 555 encode(w, r, http.StatusOK, resp) |
| paddy@131 | 556 } |
| paddy@131 | 557 |
| paddy@131 | 558 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@131 | 559 errors := []requestError{} |
| paddy@131 | 560 var err error |
| paddy@131 | 561 // BUG(paddy): If ids are provided in query params, retrieve only those clients |
| paddy@131 | 562 // BUG(paddy): We should have auth when listing clients |
| paddy@131 | 563 num := defaultClientResponseSize |
| paddy@131 | 564 offset := 0 |
| paddy@131 | 565 ownerIDStr := r.URL.Query().Get("owner_id") |
| paddy@131 | 566 numStr := r.URL.Query().Get("num") |
| paddy@131 | 567 offsetStr := r.URL.Query().Get("offset") |
| paddy@131 | 568 if numStr != "" { |
| paddy@131 | 569 num, err = strconv.Atoi(numStr) |
| paddy@131 | 570 if err != nil { |
| paddy@131 | 571 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"}) |
| paddy@131 | 572 } |
| paddy@131 | 573 if num > maxClientResponseSize { |
| paddy@131 | 574 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"}) |
| paddy@131 | 575 } |
| paddy@131 | 576 } |
| paddy@131 | 577 if offsetStr != "" { |
| paddy@131 | 578 offset, err = strconv.Atoi(offsetStr) |
| paddy@131 | 579 if err != nil { |
| paddy@131 | 580 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"}) |
| paddy@131 | 581 } |
| paddy@131 | 582 } |
| paddy@131 | 583 if ownerIDStr == "" { |
| paddy@131 | 584 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"}) |
| paddy@131 | 585 } |
| paddy@131 | 586 if len(errors) > 0 { |
| paddy@131 | 587 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 588 return |
| paddy@131 | 589 } |
| paddy@131 | 590 ownerID, err := uuid.Parse(ownerIDStr) |
| paddy@131 | 591 if err != nil { |
| paddy@131 | 592 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"}) |
| paddy@131 | 593 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 594 return |
| paddy@131 | 595 } |
| paddy@131 | 596 clients, err := c.ListClientsByOwner(ownerID, num, offset) |
| paddy@131 | 597 if err != nil { |
| paddy@131 | 598 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@131 | 599 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 600 return |
| paddy@131 | 601 } |
| paddy@131 | 602 for pos, client := range clients { |
| paddy@131 | 603 client.Secret = "" |
| paddy@131 | 604 clients[pos] = client |
| paddy@131 | 605 } |
| paddy@131 | 606 resp := response{ |
| paddy@131 | 607 Clients: clients, |
| paddy@131 | 608 Errors: errors, |
| paddy@131 | 609 } |
| paddy@131 | 610 encode(w, r, http.StatusOK, resp) |
| paddy@131 | 611 } |
| paddy@131 | 612 |
| paddy@133 | 613 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@133 | 614 errors := []requestError{} |
| paddy@133 | 615 vars := mux.Vars(r) |
| paddy@133 | 616 if _, ok := vars["id"]; !ok { |
| paddy@133 | 617 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@133 | 618 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 619 return |
| paddy@133 | 620 } |
| paddy@133 | 621 var change ClientChange |
| paddy@133 | 622 err := decode(r, &change) |
| paddy@133 | 623 if err != nil { |
| paddy@133 | 624 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"}) |
| paddy@133 | 625 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 626 return |
| paddy@133 | 627 } |
| paddy@133 | 628 errs := change.Validate() |
| paddy@133 | 629 for _, err := range errs { |
| paddy@133 | 630 switch err { |
| paddy@133 | 631 case ErrEmptyChange: |
| paddy@133 | 632 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"}) |
| paddy@133 | 633 case ErrClientNameTooShort: |
| paddy@133 | 634 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) |
| paddy@133 | 635 case ErrClientNameTooLong: |
| paddy@133 | 636 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) |
| paddy@133 | 637 case ErrClientLogoTooLong: |
| paddy@133 | 638 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"}) |
| paddy@133 | 639 case ErrClientLogoNotURL: |
| paddy@133 | 640 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"}) |
| paddy@133 | 641 case ErrClientWebsiteTooLong: |
| paddy@133 | 642 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"}) |
| paddy@133 | 643 case ErrClientWebsiteNotURL: |
| paddy@133 | 644 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"}) |
| paddy@133 | 645 default: |
| paddy@133 | 646 log.Println("Unrecognised error from client change validation:", err) |
| paddy@133 | 647 } |
| paddy@133 | 648 } |
| paddy@133 | 649 id, err := uuid.Parse(vars["id"]) |
| paddy@133 | 650 if err != nil { |
| paddy@133 | 651 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@133 | 652 } |
| paddy@133 | 653 if len(errors) > 0 { |
| paddy@133 | 654 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 655 return |
| paddy@133 | 656 } |
| paddy@133 | 657 client, err := c.GetClient(id) |
| paddy@133 | 658 if err == ErrClientNotFound { |
| paddy@133 | 659 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@133 | 660 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@133 | 661 return |
| paddy@133 | 662 } else if err != nil { |
| paddy@133 | 663 log.Println("Error retrieving client:", err) |
| paddy@133 | 664 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@133 | 665 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@133 | 666 return |
| paddy@133 | 667 } |
| paddy@133 | 668 if change.Secret != nil && client.Type == clientTypeConfidential { |
| paddy@133 | 669 secret := make([]byte, 32) |
| paddy@133 | 670 _, err = rand.Read(secret) |
| paddy@133 | 671 if err != nil { |
| paddy@133 | 672 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@133 | 673 return |
| paddy@133 | 674 } |
| paddy@133 | 675 newSecret := hex.EncodeToString(secret) |
| paddy@133 | 676 change.Secret = &newSecret |
| paddy@133 | 677 } |
| paddy@133 | 678 err = c.UpdateClient(id, change) |
| paddy@133 | 679 if err != nil { |
| paddy@133 | 680 log.Println("Error updating client:", err) |
| paddy@133 | 681 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@133 | 682 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@133 | 683 return |
| paddy@133 | 684 } |
| paddy@133 | 685 client.ApplyChange(change) |
| paddy@133 | 686 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors}) |
| paddy@133 | 687 return |
| paddy@133 | 688 } |
| paddy@133 | 689 |
| paddy@137 | 690 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@137 | 691 type addEndpointReq struct { |
| paddy@137 | 692 Endpoints []string `json:"endpoints"` |
| paddy@137 | 693 } |
| paddy@137 | 694 errors := []requestError{} |
| paddy@137 | 695 vars := mux.Vars(r) |
| paddy@137 | 696 if vars["id"] == "" { |
| paddy@137 | 697 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@137 | 698 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 699 return |
| paddy@137 | 700 } |
| paddy@137 | 701 id, err := uuid.Parse(vars["id"]) |
| paddy@137 | 702 if err != nil { |
| paddy@137 | 703 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@137 | 704 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 705 return |
| paddy@137 | 706 } |
| paddy@137 | 707 _, err = c.GetClient(id) |
| paddy@137 | 708 if err != nil { |
| paddy@137 | 709 if err == ErrClientNotFound { |
| paddy@137 | 710 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@137 | 711 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 712 return |
| paddy@137 | 713 } |
| paddy@137 | 714 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@137 | 715 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@137 | 716 return |
| paddy@137 | 717 } |
| paddy@137 | 718 var req addEndpointReq |
| paddy@137 | 719 decoder := json.NewDecoder(r.Body) |
| paddy@137 | 720 err = decoder.Decode(&req) |
| paddy@137 | 721 if err != nil { |
| paddy@137 | 722 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@137 | 723 return |
| paddy@137 | 724 } |
| paddy@137 | 725 if len(req.Endpoints) < 1 { |
| paddy@137 | 726 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"}) |
| paddy@137 | 727 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 728 return |
| paddy@137 | 729 } |
| paddy@137 | 730 endpoints := []Endpoint{} |
| paddy@137 | 731 for pos, u := range req.Endpoints { |
| paddy@137 | 732 if parsed, err := url.Parse(u); err != nil { |
| paddy@137 | 733 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@137 | 734 continue |
| paddy@137 | 735 } else if !parsed.IsAbs() { |
| paddy@137 | 736 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)}) |
| paddy@137 | 737 continue |
| paddy@137 | 738 } |
| paddy@137 | 739 e := Endpoint{ |
| paddy@137 | 740 ID: uuid.NewID(), |
| paddy@137 | 741 ClientID: id, |
| paddy@137 | 742 URI: u, |
| paddy@137 | 743 Added: time.Now(), |
| paddy@137 | 744 } |
| paddy@137 | 745 endpoints = append(endpoints, e) |
| paddy@137 | 746 } |
| paddy@137 | 747 if len(errors) > 0 { |
| paddy@137 | 748 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 749 return |
| paddy@137 | 750 } |
| paddy@137 | 751 err = c.AddEndpoints(id, endpoints) |
| paddy@137 | 752 if err != nil { |
| paddy@137 | 753 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@137 | 754 return |
| paddy@137 | 755 } |
| paddy@137 | 756 resp := response{ |
| paddy@137 | 757 Errors: errors, |
| paddy@137 | 758 Endpoints: endpoints, |
| paddy@137 | 759 } |
| paddy@137 | 760 encode(w, r, http.StatusCreated, resp) |
| paddy@137 | 761 } |
| paddy@137 | 762 |
| paddy@138 | 763 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@138 | 764 errors := []requestError{} |
| paddy@138 | 765 vars := mux.Vars(r) |
| paddy@138 | 766 clientID, err := uuid.Parse(vars["id"]) |
| paddy@138 | 767 if err != nil { |
| paddy@138 | 768 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"}) |
| paddy@138 | 769 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@138 | 770 return |
| paddy@138 | 771 } |
| paddy@138 | 772 num := defaultEndpointResponseSize |
| paddy@138 | 773 offset := 0 |
| paddy@138 | 774 numStr := r.URL.Query().Get("num") |
| paddy@138 | 775 offsetStr := r.URL.Query().Get("offset") |
| paddy@138 | 776 if numStr != "" { |
| paddy@138 | 777 num, err = strconv.Atoi(numStr) |
| paddy@138 | 778 if err != nil { |
| paddy@138 | 779 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"}) |
| paddy@138 | 780 } |
| paddy@138 | 781 if num > maxEndpointResponseSize { |
| paddy@138 | 782 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"}) |
| paddy@138 | 783 } |
| paddy@138 | 784 } |
| paddy@138 | 785 if offsetStr != "" { |
| paddy@138 | 786 offset, err = strconv.Atoi(offsetStr) |
| paddy@138 | 787 if err != nil { |
| paddy@138 | 788 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"}) |
| paddy@138 | 789 } |
| paddy@138 | 790 } |
| paddy@138 | 791 if len(errors) > 0 { |
| paddy@138 | 792 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@138 | 793 return |
| paddy@138 | 794 } |
| paddy@138 | 795 endpoints, err := c.ListEndpoints(clientID, num, offset) |
| paddy@138 | 796 if err != nil { |
| paddy@138 | 797 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@138 | 798 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@138 | 799 return |
| paddy@138 | 800 } |
| paddy@138 | 801 resp := response{ |
| paddy@138 | 802 Endpoints: endpoints, |
| paddy@138 | 803 Errors: errors, |
| paddy@138 | 804 } |
| paddy@138 | 805 encode(w, r, http.StatusOK, resp) |
| paddy@138 | 806 } |
| paddy@138 | 807 |
| paddy@135 | 808 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) { |
| paddy@135 | 809 scopes = strings.Split(r.PostFormValue("scope"), " ") |
| paddy@121 | 810 valid = true |
| paddy@121 | 811 return |
| paddy@121 | 812 } |
| paddy@124 | 813 |
| paddy@124 | 814 func clientCredentialsAuditString(r *http.Request) string { |
| paddy@124 | 815 return "client_credentials" |
| paddy@124 | 816 } |