auth
auth/client.go
Return client Secrets when listing clients with basic auth. If the request to list clients is sent with basic auth containing the login and password for the owner of the client, its secret is not removed from the response before sending it.
| 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 num := defaultClientResponseSize |
| paddy@131 | 563 offset := 0 |
| paddy@131 | 564 ownerIDStr := r.URL.Query().Get("owner_id") |
| paddy@131 | 565 numStr := r.URL.Query().Get("num") |
| paddy@131 | 566 offsetStr := r.URL.Query().Get("offset") |
| paddy@131 | 567 if numStr != "" { |
| paddy@131 | 568 num, err = strconv.Atoi(numStr) |
| paddy@131 | 569 if err != nil { |
| paddy@131 | 570 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"}) |
| paddy@131 | 571 } |
| paddy@131 | 572 if num > maxClientResponseSize { |
| paddy@131 | 573 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"}) |
| paddy@131 | 574 } |
| paddy@131 | 575 } |
| paddy@131 | 576 if offsetStr != "" { |
| paddy@131 | 577 offset, err = strconv.Atoi(offsetStr) |
| paddy@131 | 578 if err != nil { |
| paddy@131 | 579 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"}) |
| paddy@131 | 580 } |
| paddy@131 | 581 } |
| paddy@131 | 582 if ownerIDStr == "" { |
| paddy@131 | 583 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"}) |
| paddy@131 | 584 } |
| paddy@131 | 585 if len(errors) > 0 { |
| paddy@131 | 586 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 587 return |
| paddy@131 | 588 } |
| paddy@131 | 589 ownerID, err := uuid.Parse(ownerIDStr) |
| paddy@131 | 590 if err != nil { |
| paddy@131 | 591 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"}) |
| paddy@131 | 592 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 593 return |
| paddy@131 | 594 } |
| paddy@131 | 595 clients, err := c.ListClientsByOwner(ownerID, num, offset) |
| paddy@131 | 596 if err != nil { |
| paddy@131 | 597 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@131 | 598 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 599 return |
| paddy@131 | 600 } |
| paddy@140 | 601 username, password, ok := r.BasicAuth() |
| paddy@140 | 602 if !ok { |
| paddy@140 | 603 for pos, client := range clients { |
| paddy@140 | 604 client.Secret = "" |
| paddy@140 | 605 clients[pos] = client |
| paddy@140 | 606 } |
| paddy@140 | 607 } else { |
| paddy@140 | 608 profile, err := authenticate(username, password, c) |
| paddy@140 | 609 if err != nil { |
| paddy@140 | 610 if isAuthError(err) { |
| paddy@140 | 611 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@140 | 612 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@140 | 613 } else { |
| paddy@140 | 614 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@140 | 615 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@140 | 616 } |
| paddy@140 | 617 return |
| paddy@140 | 618 } |
| paddy@140 | 619 for pos, client := range clients { |
| paddy@140 | 620 if !client.OwnerID.Equal(profile.ID) { |
| paddy@140 | 621 client.Secret = "" |
| paddy@140 | 622 clients[pos] = client |
| paddy@140 | 623 } |
| paddy@140 | 624 } |
| paddy@131 | 625 } |
| paddy@131 | 626 resp := response{ |
| paddy@131 | 627 Clients: clients, |
| paddy@131 | 628 Errors: errors, |
| paddy@131 | 629 } |
| paddy@131 | 630 encode(w, r, http.StatusOK, resp) |
| paddy@131 | 631 } |
| paddy@131 | 632 |
| paddy@133 | 633 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@133 | 634 errors := []requestError{} |
| paddy@133 | 635 vars := mux.Vars(r) |
| paddy@133 | 636 if _, ok := vars["id"]; !ok { |
| paddy@133 | 637 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@133 | 638 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 639 return |
| paddy@133 | 640 } |
| paddy@133 | 641 var change ClientChange |
| paddy@133 | 642 err := decode(r, &change) |
| paddy@133 | 643 if err != nil { |
| paddy@133 | 644 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"}) |
| paddy@133 | 645 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 646 return |
| paddy@133 | 647 } |
| paddy@133 | 648 errs := change.Validate() |
| paddy@133 | 649 for _, err := range errs { |
| paddy@133 | 650 switch err { |
| paddy@133 | 651 case ErrEmptyChange: |
| paddy@133 | 652 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"}) |
| paddy@133 | 653 case ErrClientNameTooShort: |
| paddy@133 | 654 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) |
| paddy@133 | 655 case ErrClientNameTooLong: |
| paddy@133 | 656 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) |
| paddy@133 | 657 case ErrClientLogoTooLong: |
| paddy@133 | 658 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"}) |
| paddy@133 | 659 case ErrClientLogoNotURL: |
| paddy@133 | 660 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"}) |
| paddy@133 | 661 case ErrClientWebsiteTooLong: |
| paddy@133 | 662 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"}) |
| paddy@133 | 663 case ErrClientWebsiteNotURL: |
| paddy@133 | 664 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"}) |
| paddy@133 | 665 default: |
| paddy@133 | 666 log.Println("Unrecognised error from client change validation:", err) |
| paddy@133 | 667 } |
| paddy@133 | 668 } |
| paddy@133 | 669 id, err := uuid.Parse(vars["id"]) |
| paddy@133 | 670 if err != nil { |
| paddy@133 | 671 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@133 | 672 } |
| paddy@133 | 673 if len(errors) > 0 { |
| paddy@133 | 674 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 675 return |
| paddy@133 | 676 } |
| paddy@133 | 677 client, err := c.GetClient(id) |
| paddy@133 | 678 if err == ErrClientNotFound { |
| paddy@133 | 679 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@133 | 680 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@133 | 681 return |
| paddy@133 | 682 } else if err != nil { |
| paddy@133 | 683 log.Println("Error retrieving client:", err) |
| paddy@133 | 684 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@133 | 685 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@133 | 686 return |
| paddy@133 | 687 } |
| paddy@133 | 688 if change.Secret != nil && client.Type == clientTypeConfidential { |
| paddy@133 | 689 secret := make([]byte, 32) |
| paddy@133 | 690 _, err = rand.Read(secret) |
| paddy@133 | 691 if err != nil { |
| paddy@133 | 692 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@133 | 693 return |
| paddy@133 | 694 } |
| paddy@133 | 695 newSecret := hex.EncodeToString(secret) |
| paddy@133 | 696 change.Secret = &newSecret |
| paddy@133 | 697 } |
| paddy@133 | 698 err = c.UpdateClient(id, change) |
| paddy@133 | 699 if err != nil { |
| paddy@133 | 700 log.Println("Error updating client:", err) |
| paddy@133 | 701 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@133 | 702 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@133 | 703 return |
| paddy@133 | 704 } |
| paddy@133 | 705 client.ApplyChange(change) |
| paddy@133 | 706 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors}) |
| paddy@133 | 707 return |
| paddy@133 | 708 } |
| paddy@133 | 709 |
| paddy@137 | 710 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@137 | 711 type addEndpointReq struct { |
| paddy@137 | 712 Endpoints []string `json:"endpoints"` |
| paddy@137 | 713 } |
| paddy@137 | 714 errors := []requestError{} |
| paddy@137 | 715 vars := mux.Vars(r) |
| paddy@137 | 716 if vars["id"] == "" { |
| paddy@137 | 717 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@137 | 718 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 719 return |
| paddy@137 | 720 } |
| paddy@137 | 721 id, err := uuid.Parse(vars["id"]) |
| paddy@137 | 722 if err != nil { |
| paddy@137 | 723 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@137 | 724 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 725 return |
| paddy@137 | 726 } |
| paddy@137 | 727 _, err = c.GetClient(id) |
| paddy@137 | 728 if err != nil { |
| paddy@137 | 729 if err == ErrClientNotFound { |
| paddy@137 | 730 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@137 | 731 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 732 return |
| paddy@137 | 733 } |
| paddy@137 | 734 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@137 | 735 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@137 | 736 return |
| paddy@137 | 737 } |
| paddy@137 | 738 var req addEndpointReq |
| paddy@137 | 739 decoder := json.NewDecoder(r.Body) |
| paddy@137 | 740 err = decoder.Decode(&req) |
| paddy@137 | 741 if err != nil { |
| paddy@137 | 742 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@137 | 743 return |
| paddy@137 | 744 } |
| paddy@137 | 745 if len(req.Endpoints) < 1 { |
| paddy@137 | 746 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"}) |
| paddy@137 | 747 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 748 return |
| paddy@137 | 749 } |
| paddy@137 | 750 endpoints := []Endpoint{} |
| paddy@137 | 751 for pos, u := range req.Endpoints { |
| paddy@137 | 752 if parsed, err := url.Parse(u); err != nil { |
| paddy@137 | 753 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@137 | 754 continue |
| paddy@137 | 755 } else if !parsed.IsAbs() { |
| paddy@137 | 756 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)}) |
| paddy@137 | 757 continue |
| paddy@137 | 758 } |
| paddy@137 | 759 e := Endpoint{ |
| paddy@137 | 760 ID: uuid.NewID(), |
| paddy@137 | 761 ClientID: id, |
| paddy@137 | 762 URI: u, |
| paddy@137 | 763 Added: time.Now(), |
| paddy@137 | 764 } |
| paddy@137 | 765 endpoints = append(endpoints, e) |
| paddy@137 | 766 } |
| paddy@137 | 767 if len(errors) > 0 { |
| paddy@137 | 768 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 769 return |
| paddy@137 | 770 } |
| paddy@137 | 771 err = c.AddEndpoints(id, endpoints) |
| paddy@137 | 772 if err != nil { |
| paddy@137 | 773 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@137 | 774 return |
| paddy@137 | 775 } |
| paddy@137 | 776 resp := response{ |
| paddy@137 | 777 Errors: errors, |
| paddy@137 | 778 Endpoints: endpoints, |
| paddy@137 | 779 } |
| paddy@137 | 780 encode(w, r, http.StatusCreated, resp) |
| paddy@137 | 781 } |
| paddy@137 | 782 |
| paddy@138 | 783 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@138 | 784 errors := []requestError{} |
| paddy@138 | 785 vars := mux.Vars(r) |
| paddy@138 | 786 clientID, err := uuid.Parse(vars["id"]) |
| paddy@138 | 787 if err != nil { |
| paddy@138 | 788 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"}) |
| paddy@138 | 789 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@138 | 790 return |
| paddy@138 | 791 } |
| paddy@138 | 792 num := defaultEndpointResponseSize |
| paddy@138 | 793 offset := 0 |
| paddy@138 | 794 numStr := r.URL.Query().Get("num") |
| paddy@138 | 795 offsetStr := r.URL.Query().Get("offset") |
| paddy@138 | 796 if numStr != "" { |
| paddy@138 | 797 num, err = strconv.Atoi(numStr) |
| paddy@138 | 798 if err != nil { |
| paddy@138 | 799 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"}) |
| paddy@138 | 800 } |
| paddy@138 | 801 if num > maxEndpointResponseSize { |
| paddy@138 | 802 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"}) |
| paddy@138 | 803 } |
| paddy@138 | 804 } |
| paddy@138 | 805 if offsetStr != "" { |
| paddy@138 | 806 offset, err = strconv.Atoi(offsetStr) |
| paddy@138 | 807 if err != nil { |
| paddy@138 | 808 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"}) |
| paddy@138 | 809 } |
| paddy@138 | 810 } |
| paddy@138 | 811 if len(errors) > 0 { |
| paddy@138 | 812 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@138 | 813 return |
| paddy@138 | 814 } |
| paddy@138 | 815 endpoints, err := c.ListEndpoints(clientID, num, offset) |
| paddy@138 | 816 if err != nil { |
| paddy@138 | 817 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@138 | 818 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@138 | 819 return |
| paddy@138 | 820 } |
| paddy@138 | 821 resp := response{ |
| paddy@138 | 822 Endpoints: endpoints, |
| paddy@138 | 823 Errors: errors, |
| paddy@138 | 824 } |
| paddy@138 | 825 encode(w, r, http.StatusOK, resp) |
| paddy@138 | 826 } |
| paddy@138 | 827 |
| paddy@135 | 828 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) { |
| paddy@135 | 829 scopes = strings.Split(r.PostFormValue("scope"), " ") |
| paddy@121 | 830 valid = true |
| paddy@121 | 831 return |
| paddy@121 | 832 } |
| paddy@124 | 833 |
| paddy@124 | 834 func clientCredentialsAuditString(r *http.Request) string { |
| paddy@124 | 835 return "client_credentials" |
| paddy@124 | 836 } |