auth
auth/client.go
Require authentication to update Clients. Require the Client's owner to supply basic authentication when updating a client.
| 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@141 | 641 id, err := uuid.Parse(vars["id"]) |
| paddy@141 | 642 if err != nil { |
| paddy@141 | 643 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@141 | 644 } |
| paddy@141 | 645 username, password, ok := r.BasicAuth() |
| paddy@141 | 646 if !ok { |
| paddy@141 | 647 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@141 | 648 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@141 | 649 return |
| paddy@141 | 650 } |
| paddy@141 | 651 profile, err := authenticate(username, password, c) |
| paddy@141 | 652 if err != nil { |
| paddy@141 | 653 if isAuthError(err) { |
| paddy@141 | 654 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@141 | 655 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@141 | 656 } else { |
| paddy@141 | 657 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@141 | 658 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@141 | 659 } |
| paddy@141 | 660 return |
| paddy@141 | 661 } |
| paddy@133 | 662 var change ClientChange |
| paddy@141 | 663 err = decode(r, &change) |
| paddy@133 | 664 if err != nil { |
| paddy@133 | 665 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"}) |
| paddy@133 | 666 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 667 return |
| paddy@133 | 668 } |
| paddy@133 | 669 errs := change.Validate() |
| paddy@133 | 670 for _, err := range errs { |
| paddy@133 | 671 switch err { |
| paddy@133 | 672 case ErrEmptyChange: |
| paddy@133 | 673 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"}) |
| paddy@133 | 674 case ErrClientNameTooShort: |
| paddy@133 | 675 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) |
| paddy@133 | 676 case ErrClientNameTooLong: |
| paddy@133 | 677 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) |
| paddy@133 | 678 case ErrClientLogoTooLong: |
| paddy@133 | 679 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"}) |
| paddy@133 | 680 case ErrClientLogoNotURL: |
| paddy@133 | 681 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"}) |
| paddy@133 | 682 case ErrClientWebsiteTooLong: |
| paddy@133 | 683 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"}) |
| paddy@133 | 684 case ErrClientWebsiteNotURL: |
| paddy@133 | 685 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"}) |
| paddy@133 | 686 default: |
| paddy@133 | 687 log.Println("Unrecognised error from client change validation:", err) |
| paddy@133 | 688 } |
| paddy@133 | 689 } |
| paddy@133 | 690 if len(errors) > 0 { |
| paddy@133 | 691 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 692 return |
| paddy@133 | 693 } |
| paddy@133 | 694 client, err := c.GetClient(id) |
| paddy@133 | 695 if err == ErrClientNotFound { |
| paddy@133 | 696 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@133 | 697 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@133 | 698 return |
| paddy@133 | 699 } else if err != nil { |
| paddy@133 | 700 log.Println("Error retrieving 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@141 | 705 if !client.OwnerID.Equal(profile.ID) { |
| paddy@141 | 706 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@141 | 707 encode(w, r, http.StatusForbidden, response{Errors: errors}) |
| paddy@141 | 708 return |
| paddy@141 | 709 } |
| paddy@133 | 710 if change.Secret != nil && client.Type == clientTypeConfidential { |
| paddy@133 | 711 secret := make([]byte, 32) |
| paddy@133 | 712 _, err = rand.Read(secret) |
| paddy@133 | 713 if err != nil { |
| paddy@133 | 714 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@133 | 715 return |
| paddy@133 | 716 } |
| paddy@133 | 717 newSecret := hex.EncodeToString(secret) |
| paddy@133 | 718 change.Secret = &newSecret |
| paddy@133 | 719 } |
| paddy@133 | 720 err = c.UpdateClient(id, change) |
| paddy@133 | 721 if err != nil { |
| paddy@133 | 722 log.Println("Error updating client:", err) |
| paddy@133 | 723 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@133 | 724 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@133 | 725 return |
| paddy@133 | 726 } |
| paddy@133 | 727 client.ApplyChange(change) |
| paddy@133 | 728 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors}) |
| paddy@133 | 729 return |
| paddy@133 | 730 } |
| paddy@133 | 731 |
| paddy@137 | 732 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@137 | 733 type addEndpointReq struct { |
| paddy@137 | 734 Endpoints []string `json:"endpoints"` |
| paddy@137 | 735 } |
| paddy@137 | 736 errors := []requestError{} |
| paddy@137 | 737 vars := mux.Vars(r) |
| paddy@137 | 738 if vars["id"] == "" { |
| paddy@137 | 739 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@137 | 740 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 741 return |
| paddy@137 | 742 } |
| paddy@137 | 743 id, err := uuid.Parse(vars["id"]) |
| paddy@137 | 744 if err != nil { |
| paddy@137 | 745 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@137 | 746 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 747 return |
| paddy@137 | 748 } |
| paddy@137 | 749 _, err = c.GetClient(id) |
| paddy@137 | 750 if err != nil { |
| paddy@137 | 751 if err == ErrClientNotFound { |
| paddy@137 | 752 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@137 | 753 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 754 return |
| paddy@137 | 755 } |
| paddy@137 | 756 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@137 | 757 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@137 | 758 return |
| paddy@137 | 759 } |
| paddy@137 | 760 var req addEndpointReq |
| paddy@137 | 761 decoder := json.NewDecoder(r.Body) |
| paddy@137 | 762 err = decoder.Decode(&req) |
| paddy@137 | 763 if err != nil { |
| paddy@137 | 764 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@137 | 765 return |
| paddy@137 | 766 } |
| paddy@137 | 767 if len(req.Endpoints) < 1 { |
| paddy@137 | 768 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"}) |
| paddy@137 | 769 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 770 return |
| paddy@137 | 771 } |
| paddy@137 | 772 endpoints := []Endpoint{} |
| paddy@137 | 773 for pos, u := range req.Endpoints { |
| paddy@137 | 774 if parsed, err := url.Parse(u); err != nil { |
| paddy@137 | 775 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@137 | 776 continue |
| paddy@137 | 777 } else if !parsed.IsAbs() { |
| paddy@137 | 778 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)}) |
| paddy@137 | 779 continue |
| paddy@137 | 780 } |
| paddy@137 | 781 e := Endpoint{ |
| paddy@137 | 782 ID: uuid.NewID(), |
| paddy@137 | 783 ClientID: id, |
| paddy@137 | 784 URI: u, |
| paddy@137 | 785 Added: time.Now(), |
| paddy@137 | 786 } |
| paddy@137 | 787 endpoints = append(endpoints, e) |
| paddy@137 | 788 } |
| paddy@137 | 789 if len(errors) > 0 { |
| paddy@137 | 790 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 791 return |
| paddy@137 | 792 } |
| paddy@137 | 793 err = c.AddEndpoints(id, endpoints) |
| paddy@137 | 794 if err != nil { |
| paddy@137 | 795 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@137 | 796 return |
| paddy@137 | 797 } |
| paddy@137 | 798 resp := response{ |
| paddy@137 | 799 Errors: errors, |
| paddy@137 | 800 Endpoints: endpoints, |
| paddy@137 | 801 } |
| paddy@137 | 802 encode(w, r, http.StatusCreated, resp) |
| paddy@137 | 803 } |
| paddy@137 | 804 |
| paddy@138 | 805 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@138 | 806 errors := []requestError{} |
| paddy@138 | 807 vars := mux.Vars(r) |
| paddy@138 | 808 clientID, err := uuid.Parse(vars["id"]) |
| paddy@138 | 809 if err != nil { |
| paddy@138 | 810 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"}) |
| paddy@138 | 811 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@138 | 812 return |
| paddy@138 | 813 } |
| paddy@138 | 814 num := defaultEndpointResponseSize |
| paddy@138 | 815 offset := 0 |
| paddy@138 | 816 numStr := r.URL.Query().Get("num") |
| paddy@138 | 817 offsetStr := r.URL.Query().Get("offset") |
| paddy@138 | 818 if numStr != "" { |
| paddy@138 | 819 num, err = strconv.Atoi(numStr) |
| paddy@138 | 820 if err != nil { |
| paddy@138 | 821 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"}) |
| paddy@138 | 822 } |
| paddy@138 | 823 if num > maxEndpointResponseSize { |
| paddy@138 | 824 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"}) |
| paddy@138 | 825 } |
| paddy@138 | 826 } |
| paddy@138 | 827 if offsetStr != "" { |
| paddy@138 | 828 offset, err = strconv.Atoi(offsetStr) |
| paddy@138 | 829 if err != nil { |
| paddy@138 | 830 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"}) |
| paddy@138 | 831 } |
| paddy@138 | 832 } |
| paddy@138 | 833 if len(errors) > 0 { |
| paddy@138 | 834 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@138 | 835 return |
| paddy@138 | 836 } |
| paddy@138 | 837 endpoints, err := c.ListEndpoints(clientID, num, offset) |
| paddy@138 | 838 if err != nil { |
| paddy@138 | 839 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@138 | 840 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@138 | 841 return |
| paddy@138 | 842 } |
| paddy@138 | 843 resp := response{ |
| paddy@138 | 844 Endpoints: endpoints, |
| paddy@138 | 845 Errors: errors, |
| paddy@138 | 846 } |
| paddy@138 | 847 encode(w, r, http.StatusOK, resp) |
| paddy@138 | 848 } |
| paddy@138 | 849 |
| paddy@135 | 850 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) { |
| paddy@135 | 851 scopes = strings.Split(r.PostFormValue("scope"), " ") |
| paddy@121 | 852 valid = true |
| paddy@121 | 853 return |
| paddy@121 | 854 } |
| paddy@124 | 855 |
| paddy@124 | 856 func clientCredentialsAuditString(r *http.Request) string { |
| paddy@124 | 857 return "client_credentials" |
| paddy@124 | 858 } |