auth
2015-12-14
Parent:b7e685839a1b
auth/client.go
Update nsq import path. go-nsq has moved to nsqio/go-nsq, so we need to update the import path appropriately.
| 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@181 | 18 "code.secondbit.org/scopes.hg/types" |
| paddy@107 | 19 "code.secondbit.org/uuid.hg" |
| paddy@0 | 20 ) |
| paddy@0 | 21 |
| paddy@121 | 22 func init() { |
| paddy@121 | 23 RegisterGrantType("client_credentials", GrantType{ |
| paddy@121 | 24 Validate: clientCredentialsValidate, |
| paddy@121 | 25 Invalidate: nil, |
| paddy@121 | 26 IssuesRefresh: true, |
| paddy@121 | 27 ReturnToken: RenderJSONToken, |
| paddy@123 | 28 AllowsPublic: false, |
| paddy@124 | 29 AuditString: clientCredentialsAuditString, |
| paddy@121 | 30 }) |
| paddy@121 | 31 } |
| paddy@121 | 32 |
| paddy@31 | 33 var ( |
| paddy@57 | 34 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first. |
| paddy@57 | 35 ErrNoClientStore = errors.New("no clientStore was specified for the Context") |
| paddy@57 | 36 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore. |
| paddy@57 | 37 ErrClientNotFound = errors.New("client not found in clientStore") |
| paddy@57 | 38 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with |
| paddy@57 | 39 // the same ID already exists in the clientStore. |
| paddy@57 | 40 ErrClientAlreadyExists = errors.New("client already exists in clientStore") |
| paddy@143 | 41 // ErrEndpointNotFound is returned when an Endpoint is requested but not found in a clientSTore. |
| paddy@143 | 42 ErrEndpointNotFound = errors.New("endpoint not found in clientStore") |
| paddy@151 | 43 // ErrEndpointAlreadyExists is returned when an Endpoint is added to a clientStore, but another Endpoint |
| paddy@151 | 44 // with the same ID already exists in the clientStore. |
| paddy@151 | 45 ErrEndpointAlreadyExists = errors.New("endpoint already exists in clientStore") |
| paddy@41 | 46 |
| paddy@57 | 47 // ErrEmptyChange is returned when a Change has all its properties set to nil. |
| paddy@57 | 48 ErrEmptyChange = errors.New("change must have at least one property set") |
| paddy@57 | 49 // ErrClientNameTooShort is returned when a Client's Name property is too short. |
| paddy@57 | 50 ErrClientNameTooShort = errors.New("client name must be at least 2 characters") |
| paddy@57 | 51 // ErrClientNameTooLong is returned when a Client's Name property is too long. |
| paddy@57 | 52 ErrClientNameTooLong = errors.New("client name must be at most 32 characters") |
| paddy@57 | 53 // ErrClientLogoTooLong is returned when a Client's Logo property is too long. |
| paddy@57 | 54 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters") |
| paddy@57 | 55 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL. |
| paddy@57 | 56 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL") |
| paddy@57 | 57 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long. |
| paddy@49 | 58 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters") |
| paddy@57 | 59 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL. |
| paddy@57 | 60 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL") |
| paddy@116 | 61 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL. |
| paddy@116 | 62 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL") |
| paddy@31 | 63 ) |
| paddy@31 | 64 |
| paddy@115 | 65 const ( |
| paddy@138 | 66 clientTypePublic = "public" |
| paddy@138 | 67 clientTypeConfidential = "confidential" |
| paddy@138 | 68 minClientNameLen = 2 |
| paddy@138 | 69 maxClientNameLen = 24 |
| paddy@138 | 70 defaultClientResponseSize = 20 |
| paddy@138 | 71 maxClientResponseSize = 50 |
| paddy@138 | 72 defaultEndpointResponseSize = 20 |
| paddy@138 | 73 maxEndpointResponseSize = 50 |
| paddy@130 | 74 |
| paddy@130 | 75 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery |
| paddy@115 | 76 ) |
| paddy@115 | 77 |
| paddy@25 | 78 // Client represents a client that grants access |
| paddy@25 | 79 // to the auth server, exchanging grants for tokens, |
| paddy@25 | 80 // and tokens for access. |
| paddy@0 | 81 type Client struct { |
| paddy@116 | 82 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 83 Secret string `json:"secret,omitempty"` |
| paddy@116 | 84 OwnerID uuid.ID `json:"owner_id,omitempty"` |
| paddy@116 | 85 Name string `json:"name,omitempty"` |
| paddy@116 | 86 Logo string `json:"logo,omitempty"` |
| paddy@116 | 87 Website string `json:"website,omitempty"` |
| paddy@116 | 88 Type string `json:"type,omitempty"` |
| paddy@151 | 89 Deleted bool `json:"deleted,omitempty"` |
| paddy@0 | 90 } |
| paddy@0 | 91 |
| paddy@57 | 92 // ApplyChange applies the properties of the passed |
| paddy@57 | 93 // ClientChange to the Client object it is called on. |
| paddy@39 | 94 func (c *Client) ApplyChange(change ClientChange) { |
| paddy@39 | 95 if change.Secret != nil { |
| paddy@39 | 96 c.Secret = *change.Secret |
| paddy@39 | 97 } |
| paddy@39 | 98 if change.OwnerID != nil { |
| paddy@39 | 99 c.OwnerID = change.OwnerID |
| paddy@39 | 100 } |
| paddy@39 | 101 if change.Name != nil { |
| paddy@39 | 102 c.Name = *change.Name |
| paddy@39 | 103 } |
| paddy@39 | 104 if change.Logo != nil { |
| paddy@39 | 105 c.Logo = *change.Logo |
| paddy@39 | 106 } |
| paddy@39 | 107 if change.Website != nil { |
| paddy@39 | 108 c.Website = *change.Website |
| paddy@39 | 109 } |
| paddy@151 | 110 if change.Deleted != nil { |
| paddy@151 | 111 c.Deleted = *change.Deleted |
| paddy@151 | 112 } |
| paddy@39 | 113 } |
| paddy@39 | 114 |
| paddy@57 | 115 // ClientChange represents a bundle of options for |
| paddy@57 | 116 // updating a Client's mutable data. |
| paddy@31 | 117 type ClientChange struct { |
| paddy@41 | 118 Secret *string |
| paddy@41 | 119 OwnerID uuid.ID |
| paddy@41 | 120 Name *string |
| paddy@41 | 121 Logo *string |
| paddy@41 | 122 Website *string |
| paddy@151 | 123 Deleted *bool |
| paddy@151 | 124 } |
| paddy@151 | 125 |
| paddy@151 | 126 func (c ClientChange) Empty() bool { |
| paddy@151 | 127 return c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil && c.Deleted == nil |
| paddy@31 | 128 } |
| paddy@31 | 129 |
| paddy@57 | 130 // Validate checks the ClientChange it is called on |
| paddy@57 | 131 // and asserts its internal validity, or lack thereof. |
| paddy@133 | 132 func (c ClientChange) Validate() []error { |
| paddy@133 | 133 errors := []error{} |
| paddy@151 | 134 if c.Empty() { |
| paddy@133 | 135 errors = append(errors, ErrEmptyChange) |
| paddy@133 | 136 return errors |
| paddy@42 | 137 } |
| paddy@41 | 138 if c.Name != nil && len(*c.Name) < 2 { |
| paddy@133 | 139 errors = append(errors, ErrClientNameTooShort) |
| paddy@41 | 140 } |
| paddy@41 | 141 if c.Name != nil && len(*c.Name) > 32 { |
| paddy@133 | 142 errors = append(errors, ErrClientNameTooLong) |
| paddy@41 | 143 } |
| paddy@42 | 144 if c.Logo != nil && *c.Logo != "" { |
| paddy@42 | 145 if len(*c.Logo) > 1024 { |
| paddy@133 | 146 errors = append(errors, ErrClientLogoTooLong) |
| paddy@42 | 147 } |
| paddy@42 | 148 u, err := url.Parse(*c.Logo) |
| paddy@42 | 149 if err != nil || !u.IsAbs() { |
| paddy@133 | 150 errors = append(errors, ErrClientLogoNotURL) |
| paddy@42 | 151 } |
| paddy@41 | 152 } |
| paddy@42 | 153 if c.Website != nil && *c.Website != "" { |
| paddy@42 | 154 if len(*c.Website) > 140 { |
| paddy@133 | 155 errors = append(errors, ErrClientWebsiteTooLong) |
| paddy@42 | 156 } |
| paddy@42 | 157 u, err := url.Parse(*c.Website) |
| paddy@42 | 158 if err != nil || !u.IsAbs() { |
| paddy@133 | 159 errors = append(errors, ErrClientWebsiteNotURL) |
| paddy@42 | 160 } |
| paddy@41 | 161 } |
| paddy@133 | 162 return errors |
| paddy@39 | 163 } |
| paddy@39 | 164 |
| paddy@123 | 165 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) { |
| paddy@85 | 166 enc := json.NewEncoder(w) |
| paddy@85 | 167 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 168 if !fromAuthHeader { |
| paddy@85 | 169 clientIDStr = r.PostFormValue("client_id") |
| paddy@85 | 170 } |
| paddy@123 | 171 if clientIDStr == "" { |
| paddy@85 | 172 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 173 if fromAuthHeader { |
| paddy@85 | 174 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 175 } |
| paddy@85 | 176 renderJSONError(enc, "invalid_client") |
| paddy@123 | 177 return nil, "", false |
| paddy@123 | 178 } |
| paddy@129 | 179 if !allowPublic && !fromAuthHeader { |
| paddy@129 | 180 w.WriteHeader(http.StatusBadRequest) |
| paddy@129 | 181 renderJSONError(enc, "unauthorized_client") |
| paddy@129 | 182 return nil, "", false |
| paddy@129 | 183 } |
| paddy@123 | 184 clientID, err := uuid.Parse(clientIDStr) |
| paddy@123 | 185 if err != nil { |
| paddy@123 | 186 log.Println("Error decoding client ID:", err) |
| paddy@123 | 187 w.WriteHeader(http.StatusUnauthorized) |
| paddy@123 | 188 if fromAuthHeader { |
| paddy@123 | 189 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@123 | 190 } |
| paddy@123 | 191 renderJSONError(enc, "invalid_client") |
| paddy@123 | 192 return nil, "", false |
| paddy@123 | 193 } |
| paddy@123 | 194 return clientID, clientSecret, true |
| paddy@123 | 195 } |
| paddy@123 | 196 |
| paddy@123 | 197 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) { |
| paddy@123 | 198 enc := json.NewEncoder(w) |
| paddy@123 | 199 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic) |
| paddy@123 | 200 if !ok { |
| paddy@85 | 201 return nil, false |
| paddy@85 | 202 } |
| paddy@123 | 203 _, _, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 204 client, err := context.GetClient(clientID) |
| paddy@85 | 205 if err == ErrClientNotFound { |
| paddy@85 | 206 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 207 if fromAuthHeader { |
| paddy@85 | 208 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 209 } |
| paddy@85 | 210 renderJSONError(enc, "invalid_client") |
| paddy@85 | 211 return nil, false |
| paddy@85 | 212 } else if err != nil { |
| paddy@85 | 213 w.WriteHeader(http.StatusInternalServerError) |
| paddy@85 | 214 renderJSONError(enc, "server_error") |
| paddy@85 | 215 return nil, false |
| paddy@85 | 216 } |
| paddy@113 | 217 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret. |
| paddy@85 | 218 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 219 if fromAuthHeader { |
| paddy@85 | 220 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 221 } |
| paddy@85 | 222 renderJSONError(enc, "invalid_client") |
| paddy@85 | 223 return nil, false |
| paddy@85 | 224 } |
| paddy@85 | 225 return clientID, true |
| paddy@85 | 226 } |
| paddy@85 | 227 |
| paddy@57 | 228 // Endpoint represents a single URI that a Client |
| paddy@57 | 229 // controls. Users will be redirected to these URIs |
| paddy@57 | 230 // following successful authorization grants and |
| paddy@57 | 231 // exchanges for access tokens. |
| paddy@41 | 232 type Endpoint struct { |
| paddy@116 | 233 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 234 ClientID uuid.ID `json:"client_id,omitempty"` |
| paddy@116 | 235 URI string `json:"uri,omitempty"` |
| paddy@116 | 236 NormalizedURI string `json:"-"` |
| paddy@116 | 237 Added time.Time `json:"added,omitempty"` |
| paddy@116 | 238 } |
| paddy@116 | 239 |
| paddy@116 | 240 func normalizeURIString(in string) (string, error) { |
| paddy@130 | 241 n, err := purell.NormalizeURLString(in, normalizeFlags) |
| paddy@116 | 242 if err != nil { |
| paddy@116 | 243 log.Println(err) |
| paddy@116 | 244 return in, ErrEndpointURINotURL |
| paddy@116 | 245 } |
| paddy@116 | 246 return n, nil |
| paddy@116 | 247 } |
| paddy@116 | 248 |
| paddy@116 | 249 func normalizeURI(in *url.URL) string { |
| paddy@130 | 250 return purell.NormalizeURL(in, normalizeFlags) |
| paddy@41 | 251 } |
| paddy@41 | 252 |
| paddy@41 | 253 type sortedEndpoints []Endpoint |
| paddy@41 | 254 |
| paddy@41 | 255 func (s sortedEndpoints) Len() int { |
| paddy@41 | 256 return len(s) |
| paddy@41 | 257 } |
| paddy@41 | 258 |
| paddy@41 | 259 func (s sortedEndpoints) Less(i, j int) bool { |
| paddy@41 | 260 return s[i].Added.Before(s[j].Added) |
| paddy@41 | 261 } |
| paddy@41 | 262 |
| paddy@41 | 263 func (s sortedEndpoints) Swap(i, j int) { |
| paddy@41 | 264 s[i], s[j] = s[j], s[i] |
| paddy@41 | 265 } |
| paddy@41 | 266 |
| paddy@57 | 267 type clientStore interface { |
| paddy@57 | 268 getClient(id uuid.ID) (Client, error) |
| paddy@57 | 269 saveClient(client Client) error |
| paddy@57 | 270 updateClient(id uuid.ID, change ClientChange) error |
| paddy@57 | 271 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) |
| paddy@164 | 272 deleteClientsByOwner(ownerID uuid.ID) error |
| paddy@41 | 273 |
| paddy@151 | 274 addEndpoints(endpoint []Endpoint) error |
| paddy@57 | 275 removeEndpoint(client, endpoint uuid.ID) error |
| paddy@143 | 276 getEndpoint(client, endpoint uuid.ID) (Endpoint, error) |
| paddy@58 | 277 checkEndpoint(client uuid.ID, endpoint string) (bool, error) |
| paddy@57 | 278 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) |
| paddy@164 | 279 removeEndpointsByClientID(client uuid.ID) error |
| paddy@57 | 280 countEndpoints(client uuid.ID) (int64, error) |
| paddy@0 | 281 } |
| paddy@31 | 282 |
| paddy@57 | 283 func (m *memstore) getClient(id uuid.ID) (Client, error) { |
| paddy@31 | 284 m.clientLock.RLock() |
| paddy@31 | 285 defer m.clientLock.RUnlock() |
| paddy@31 | 286 c, ok := m.clients[id.String()] |
| paddy@151 | 287 if !ok || c.Deleted { |
| paddy@31 | 288 return Client{}, ErrClientNotFound |
| paddy@31 | 289 } |
| paddy@31 | 290 return c, nil |
| paddy@31 | 291 } |
| paddy@31 | 292 |
| paddy@57 | 293 func (m *memstore) saveClient(client Client) error { |
| paddy@31 | 294 m.clientLock.Lock() |
| paddy@31 | 295 defer m.clientLock.Unlock() |
| paddy@31 | 296 if _, ok := m.clients[client.ID.String()]; ok { |
| paddy@31 | 297 return ErrClientAlreadyExists |
| paddy@31 | 298 } |
| paddy@31 | 299 m.clients[client.ID.String()] = client |
| paddy@31 | 300 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID) |
| paddy@31 | 301 return nil |
| paddy@31 | 302 } |
| paddy@31 | 303 |
| paddy@57 | 304 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error { |
| paddy@39 | 305 m.clientLock.Lock() |
| paddy@39 | 306 defer m.clientLock.Unlock() |
| paddy@39 | 307 c, ok := m.clients[id.String()] |
| paddy@39 | 308 if !ok { |
| paddy@39 | 309 return ErrClientNotFound |
| paddy@39 | 310 } |
| paddy@39 | 311 c.ApplyChange(change) |
| paddy@39 | 312 m.clients[id.String()] = c |
| paddy@31 | 313 return nil |
| paddy@31 | 314 } |
| paddy@31 | 315 |
| paddy@57 | 316 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) { |
| paddy@33 | 317 ids := m.lookupClientsByProfileID(ownerID.String()) |
| paddy@164 | 318 if len(ids) > num+offset && num > 0 { |
| paddy@31 | 319 ids = ids[offset : num+offset] |
| paddy@31 | 320 } else if len(ids) > offset { |
| paddy@31 | 321 ids = ids[offset:] |
| paddy@31 | 322 } else { |
| paddy@31 | 323 return []Client{}, nil |
| paddy@31 | 324 } |
| paddy@31 | 325 clients := []Client{} |
| paddy@31 | 326 for _, id := range ids { |
| paddy@57 | 327 client, err := m.getClient(id) |
| paddy@31 | 328 if err != nil { |
| paddy@151 | 329 if err == ErrClientNotFound { |
| paddy@151 | 330 continue |
| paddy@151 | 331 } |
| paddy@31 | 332 return []Client{}, err |
| paddy@31 | 333 } |
| paddy@31 | 334 clients = append(clients, client) |
| paddy@31 | 335 } |
| paddy@31 | 336 return clients, nil |
| paddy@31 | 337 } |
| paddy@41 | 338 |
| paddy@164 | 339 func (m *memstore) deleteClientsByOwner(ownerID uuid.ID) error { |
| paddy@164 | 340 ids := m.lookupClientsByProfileID(ownerID.String()) |
| paddy@164 | 341 m.clientLock.Lock() |
| paddy@164 | 342 defer m.clientLock.RUnlock() |
| paddy@164 | 343 for _, id := range ids { |
| paddy@164 | 344 client, ok := m.clients[id.String()] |
| paddy@164 | 345 if !ok { |
| paddy@164 | 346 continue |
| paddy@164 | 347 } |
| paddy@164 | 348 client.Deleted = true |
| paddy@164 | 349 m.clients[id.String()] = client |
| paddy@164 | 350 } |
| paddy@164 | 351 return nil |
| paddy@164 | 352 } |
| paddy@164 | 353 |
| paddy@151 | 354 func (m *memstore) addEndpoints(endpoints []Endpoint) error { |
| paddy@41 | 355 m.endpointLock.Lock() |
| paddy@41 | 356 defer m.endpointLock.Unlock() |
| paddy@151 | 357 clients := map[string][]Endpoint{} |
| paddy@151 | 358 for _, endpoint := range endpoints { |
| paddy@151 | 359 clients[endpoint.ClientID.String()] = append(clients[endpoint.ClientID.String()], endpoint) |
| paddy@151 | 360 } |
| paddy@151 | 361 for client, e := range clients { |
| paddy@151 | 362 m.endpoints[client] = append(m.endpoints[client], e...) |
| paddy@151 | 363 } |
| paddy@41 | 364 return nil |
| paddy@41 | 365 } |
| paddy@41 | 366 |
| paddy@143 | 367 func (m *memstore) getEndpoint(client, endpoint uuid.ID) (Endpoint, error) { |
| paddy@143 | 368 m.endpointLock.Lock() |
| paddy@143 | 369 defer m.endpointLock.Unlock() |
| paddy@143 | 370 for _, item := range m.endpoints[client.String()] { |
| paddy@143 | 371 if item.ID.Equal(endpoint) { |
| paddy@143 | 372 return item, nil |
| paddy@143 | 373 } |
| paddy@143 | 374 } |
| paddy@143 | 375 return Endpoint{}, ErrEndpointNotFound |
| paddy@143 | 376 } |
| paddy@143 | 377 |
| paddy@57 | 378 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error { |
| paddy@41 | 379 m.endpointLock.Lock() |
| paddy@41 | 380 defer m.endpointLock.Unlock() |
| paddy@41 | 381 pos := -1 |
| paddy@41 | 382 for p, item := range m.endpoints[client.String()] { |
| paddy@41 | 383 if item.ID.Equal(endpoint) { |
| paddy@41 | 384 pos = p |
| paddy@41 | 385 break |
| paddy@41 | 386 } |
| paddy@41 | 387 } |
| paddy@41 | 388 if pos >= 0 { |
| paddy@41 | 389 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...) |
| paddy@41 | 390 } |
| paddy@41 | 391 return nil |
| paddy@41 | 392 } |
| paddy@41 | 393 |
| paddy@58 | 394 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) { |
| paddy@41 | 395 m.endpointLock.RLock() |
| paddy@41 | 396 defer m.endpointLock.RUnlock() |
| paddy@41 | 397 for _, candidate := range m.endpoints[client.String()] { |
| paddy@116 | 398 if endpoint == candidate.NormalizedURI { |
| paddy@41 | 399 return true, nil |
| paddy@41 | 400 } |
| paddy@41 | 401 } |
| paddy@41 | 402 return false, nil |
| paddy@41 | 403 } |
| paddy@41 | 404 |
| paddy@57 | 405 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) { |
| paddy@41 | 406 m.endpointLock.RLock() |
| paddy@41 | 407 defer m.endpointLock.RUnlock() |
| paddy@41 | 408 return m.endpoints[client.String()], nil |
| paddy@41 | 409 } |
| paddy@54 | 410 |
| paddy@164 | 411 func (m *memstore) removeEndpointsByClientID(client uuid.ID) error { |
| paddy@164 | 412 m.endpointLock.Lock() |
| paddy@164 | 413 defer m.endpointLock.Unlock() |
| paddy@164 | 414 delete(m.endpoints, client.String()) |
| paddy@164 | 415 return nil |
| paddy@164 | 416 } |
| paddy@164 | 417 |
| paddy@57 | 418 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) { |
| paddy@54 | 419 m.endpointLock.RLock() |
| paddy@54 | 420 defer m.endpointLock.RUnlock() |
| paddy@54 | 421 return int64(len(m.endpoints[client.String()])), nil |
| paddy@54 | 422 } |
| paddy@108 | 423 |
| paddy@164 | 424 func cleanUpAfterClientDeletion(client uuid.ID, context Context) { |
| paddy@164 | 425 err := context.RemoveEndpointsByClientID(client) |
| paddy@164 | 426 if err != nil { |
| paddy@164 | 427 log.Printf("Error removing endpoints from client %s: %+v\n", client, err) |
| paddy@164 | 428 } |
| paddy@164 | 429 err = context.DeleteAuthorizationCodesByClientID(client) |
| paddy@164 | 430 if err != nil { |
| paddy@164 | 431 log.Printf("Error removing auth codes belonging to client %s: %+v\n", client, err) |
| paddy@164 | 432 } |
| paddy@164 | 433 err = context.RevokeTokensByClientID(client) |
| paddy@164 | 434 if err != nil { |
| paddy@164 | 435 log.Printf("Error revoking tokens belonging to client %s: %+v\n", client, err) |
| paddy@164 | 436 } |
| paddy@164 | 437 } |
| paddy@164 | 438 |
| paddy@108 | 439 type newClientReq struct { |
| paddy@108 | 440 Name string `json:"name"` |
| paddy@108 | 441 Logo string `json:"logo"` |
| paddy@108 | 442 Website string `json:"website"` |
| paddy@108 | 443 Type string `json:"type"` |
| paddy@108 | 444 Endpoints []string `json:"endpoints"` |
| paddy@108 | 445 } |
| paddy@108 | 446 |
| paddy@108 | 447 func RegisterClientHandlers(r *mux.Router, context Context) { |
| paddy@108 | 448 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST") |
| paddy@131 | 449 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET") |
| paddy@131 | 450 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET") |
| paddy@133 | 451 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH") |
| paddy@144 | 452 r.Handle("/clients/{id}", wrap(context, RemoveClientHandler)).Methods("DELETE") |
| paddy@137 | 453 r.Handle("/clients/{id}/endpoints", wrap(context, AddEndpointsHandler)).Methods("POST") |
| paddy@144 | 454 r.Handle("/clients/{client_id}/endpoints/{id}", wrap(context, RemoveEndpointHandler)).Methods("DELETE") |
| paddy@138 | 455 r.Handle("/clients/{id}/endpoints", wrap(context, ListEndpointsHandler)).Methods("GET") |
| paddy@108 | 456 } |
| paddy@108 | 457 |
| paddy@108 | 458 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@172 | 459 errors := []RequestError{} |
| paddy@108 | 460 username, password, ok := r.BasicAuth() |
| paddy@108 | 461 if !ok { |
| paddy@172 | 462 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 463 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@108 | 464 return |
| paddy@108 | 465 } |
| paddy@108 | 466 profile, err := authenticate(username, password, c) |
| paddy@108 | 467 if err != nil { |
| paddy@139 | 468 if isAuthError(err) { |
| paddy@172 | 469 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 470 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@139 | 471 } else { |
| paddy@149 | 472 log.Printf("Error authenticating: %#+v\n", err) |
| paddy@172 | 473 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 474 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@139 | 475 } |
| paddy@108 | 476 return |
| paddy@108 | 477 } |
| paddy@108 | 478 var req newClientReq |
| paddy@108 | 479 decoder := json.NewDecoder(r.Body) |
| paddy@108 | 480 err = decoder.Decode(&req) |
| paddy@108 | 481 if err != nil { |
| paddy@108 | 482 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@108 | 483 return |
| paddy@108 | 484 } |
| paddy@116 | 485 if req.Type == "" { |
| paddy@172 | 486 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/type"}) |
| paddy@116 | 487 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential { |
| paddy@172 | 488 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/type"}) |
| paddy@116 | 489 } |
| paddy@116 | 490 if req.Name == "" { |
| paddy@172 | 491 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/name"}) |
| paddy@116 | 492 } else if len(req.Name) < minClientNameLen { |
| paddy@172 | 493 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/name"}) |
| paddy@116 | 494 } else if len(req.Name) > maxClientNameLen { |
| paddy@172 | 495 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/name"}) |
| paddy@116 | 496 } |
| paddy@116 | 497 if len(errors) > 0 { |
| paddy@172 | 498 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@108 | 499 return |
| paddy@108 | 500 } |
| paddy@108 | 501 client := Client{ |
| paddy@108 | 502 ID: uuid.NewID(), |
| paddy@108 | 503 OwnerID: profile.ID, |
| paddy@108 | 504 Name: req.Name, |
| paddy@108 | 505 Logo: req.Logo, |
| paddy@108 | 506 Website: req.Website, |
| paddy@108 | 507 Type: req.Type, |
| paddy@108 | 508 } |
| paddy@118 | 509 if client.Type == clientTypeConfidential { |
| paddy@115 | 510 secret := make([]byte, 32) |
| paddy@115 | 511 _, err = rand.Read(secret) |
| paddy@115 | 512 if err != nil { |
| paddy@149 | 513 log.Printf("Error generating secret: %#+v\n", err) |
| paddy@115 | 514 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@115 | 515 return |
| paddy@115 | 516 } |
| paddy@115 | 517 client.Secret = hex.EncodeToString(secret) |
| paddy@115 | 518 } |
| paddy@108 | 519 err = c.SaveClient(client) |
| paddy@108 | 520 if err != nil { |
| paddy@115 | 521 if err == ErrClientAlreadyExists { |
| paddy@172 | 522 errors = append(errors, RequestError{Slug: RequestErrConflict, Field: "/id"}) |
| paddy@172 | 523 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@115 | 524 return |
| paddy@115 | 525 } |
| paddy@149 | 526 log.Printf("Error saving client: %#+v\n", err) |
| paddy@115 | 527 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@108 | 528 return |
| paddy@108 | 529 } |
| paddy@108 | 530 endpoints := []Endpoint{} |
| paddy@115 | 531 for pos, u := range req.Endpoints { |
| paddy@108 | 532 uri, err := url.Parse(u) |
| paddy@108 | 533 if err != nil { |
| paddy@172 | 534 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@108 | 535 continue |
| paddy@108 | 536 } |
| paddy@116 | 537 if !uri.IsAbs() { |
| paddy@172 | 538 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@116 | 539 continue |
| paddy@116 | 540 } |
| paddy@108 | 541 endpoint := Endpoint{ |
| paddy@108 | 542 ID: uuid.NewID(), |
| paddy@108 | 543 ClientID: client.ID, |
| paddy@116 | 544 URI: uri.String(), |
| paddy@108 | 545 Added: time.Now(), |
| paddy@108 | 546 } |
| paddy@108 | 547 endpoints = append(endpoints, endpoint) |
| paddy@108 | 548 } |
| paddy@151 | 549 err = c.AddEndpoints(endpoints) |
| paddy@115 | 550 if err != nil { |
| paddy@149 | 551 log.Printf("Error adding endpoints: %#+v\n", err) |
| paddy@172 | 552 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 553 encode(w, r, http.StatusInternalServerError, Response{Errors: errors, Clients: []Client{client}}) |
| paddy@115 | 554 return |
| paddy@115 | 555 } |
| paddy@172 | 556 resp := Response{ |
| paddy@108 | 557 Clients: []Client{client}, |
| paddy@108 | 558 Endpoints: endpoints, |
| paddy@116 | 559 Errors: errors, |
| paddy@108 | 560 } |
| paddy@108 | 561 encode(w, r, http.StatusCreated, resp) |
| paddy@108 | 562 } |
| paddy@121 | 563 |
| paddy@131 | 564 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@172 | 565 errors := []RequestError{} |
| paddy@131 | 566 vars := mux.Vars(r) |
| paddy@131 | 567 if vars["id"] == "" { |
| paddy@172 | 568 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) |
| paddy@172 | 569 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@131 | 570 return |
| paddy@131 | 571 } |
| paddy@131 | 572 id, err := uuid.Parse(vars["id"]) |
| paddy@131 | 573 if err != nil { |
| paddy@172 | 574 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"}) |
| paddy@172 | 575 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@131 | 576 return |
| paddy@131 | 577 } |
| paddy@131 | 578 client, err := c.GetClient(id) |
| paddy@131 | 579 if err != nil { |
| paddy@131 | 580 if err == ErrClientNotFound { |
| paddy@172 | 581 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) |
| paddy@172 | 582 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@131 | 583 return |
| paddy@131 | 584 } |
| paddy@172 | 585 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 586 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@131 | 587 return |
| paddy@131 | 588 } |
| paddy@139 | 589 username, password, ok := r.BasicAuth() |
| paddy@139 | 590 if !ok { |
| paddy@139 | 591 client.Secret = "" |
| paddy@139 | 592 } else { |
| paddy@139 | 593 profile, err := authenticate(username, password, c) |
| paddy@139 | 594 if err != nil { |
| paddy@139 | 595 if isAuthError(err) { |
| paddy@172 | 596 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 597 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@139 | 598 } else { |
| paddy@172 | 599 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 600 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@139 | 601 } |
| paddy@139 | 602 return |
| paddy@139 | 603 } |
| paddy@139 | 604 if !client.OwnerID.Equal(profile.ID) { |
| paddy@139 | 605 client.Secret = "" |
| paddy@139 | 606 } |
| paddy@139 | 607 } |
| paddy@172 | 608 resp := Response{ |
| paddy@131 | 609 Clients: []Client{client}, |
| paddy@131 | 610 Errors: errors, |
| paddy@131 | 611 } |
| paddy@131 | 612 encode(w, r, http.StatusOK, resp) |
| paddy@131 | 613 } |
| paddy@131 | 614 |
| paddy@131 | 615 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@172 | 616 errors := []RequestError{} |
| paddy@131 | 617 var err error |
| paddy@131 | 618 // BUG(paddy): If ids are provided in query params, retrieve only those clients |
| paddy@131 | 619 num := defaultClientResponseSize |
| paddy@131 | 620 offset := 0 |
| paddy@131 | 621 ownerIDStr := r.URL.Query().Get("owner_id") |
| paddy@131 | 622 numStr := r.URL.Query().Get("num") |
| paddy@131 | 623 offsetStr := r.URL.Query().Get("offset") |
| paddy@131 | 624 if numStr != "" { |
| paddy@131 | 625 num, err = strconv.Atoi(numStr) |
| paddy@131 | 626 if err != nil { |
| paddy@172 | 627 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "num"}) |
| paddy@131 | 628 } |
| paddy@131 | 629 if num > maxClientResponseSize { |
| paddy@172 | 630 errors = append(errors, RequestError{Slug: RequestErrOverflow, Param: "num"}) |
| paddy@131 | 631 } |
| paddy@164 | 632 if num < 1 { |
| paddy@172 | 633 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Param: "num"}) |
| paddy@164 | 634 } |
| paddy@131 | 635 } |
| paddy@131 | 636 if offsetStr != "" { |
| paddy@131 | 637 offset, err = strconv.Atoi(offsetStr) |
| paddy@131 | 638 if err != nil { |
| paddy@172 | 639 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "offset"}) |
| paddy@131 | 640 } |
| paddy@131 | 641 } |
| paddy@131 | 642 if ownerIDStr == "" { |
| paddy@172 | 643 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "owner_id"}) |
| paddy@131 | 644 } |
| paddy@131 | 645 if len(errors) > 0 { |
| paddy@172 | 646 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@131 | 647 return |
| paddy@131 | 648 } |
| paddy@131 | 649 ownerID, err := uuid.Parse(ownerIDStr) |
| paddy@131 | 650 if err != nil { |
| paddy@172 | 651 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "owner_id"}) |
| paddy@172 | 652 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@131 | 653 return |
| paddy@131 | 654 } |
| paddy@131 | 655 clients, err := c.ListClientsByOwner(ownerID, num, offset) |
| paddy@131 | 656 if err != nil { |
| paddy@172 | 657 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 658 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@131 | 659 return |
| paddy@131 | 660 } |
| paddy@140 | 661 username, password, ok := r.BasicAuth() |
| paddy@140 | 662 if !ok { |
| paddy@140 | 663 for pos, client := range clients { |
| paddy@140 | 664 client.Secret = "" |
| paddy@140 | 665 clients[pos] = client |
| paddy@140 | 666 } |
| paddy@140 | 667 } else { |
| paddy@140 | 668 profile, err := authenticate(username, password, c) |
| paddy@140 | 669 if err != nil { |
| paddy@140 | 670 if isAuthError(err) { |
| paddy@172 | 671 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 672 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@140 | 673 } else { |
| paddy@172 | 674 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 675 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@140 | 676 } |
| paddy@140 | 677 return |
| paddy@140 | 678 } |
| paddy@140 | 679 for pos, client := range clients { |
| paddy@140 | 680 if !client.OwnerID.Equal(profile.ID) { |
| paddy@140 | 681 client.Secret = "" |
| paddy@140 | 682 clients[pos] = client |
| paddy@140 | 683 } |
| paddy@140 | 684 } |
| paddy@131 | 685 } |
| paddy@172 | 686 resp := Response{ |
| paddy@131 | 687 Clients: clients, |
| paddy@131 | 688 Errors: errors, |
| paddy@131 | 689 } |
| paddy@131 | 690 encode(w, r, http.StatusOK, resp) |
| paddy@131 | 691 } |
| paddy@131 | 692 |
| paddy@133 | 693 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@172 | 694 errors := []RequestError{} |
| paddy@133 | 695 vars := mux.Vars(r) |
| paddy@133 | 696 if _, ok := vars["id"]; !ok { |
| paddy@172 | 697 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) |
| paddy@172 | 698 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@133 | 699 return |
| paddy@133 | 700 } |
| paddy@141 | 701 id, err := uuid.Parse(vars["id"]) |
| paddy@141 | 702 if err != nil { |
| paddy@172 | 703 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"}) |
| paddy@141 | 704 } |
| paddy@141 | 705 username, password, ok := r.BasicAuth() |
| paddy@141 | 706 if !ok { |
| paddy@172 | 707 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 708 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@141 | 709 return |
| paddy@141 | 710 } |
| paddy@141 | 711 profile, err := authenticate(username, password, c) |
| paddy@141 | 712 if err != nil { |
| paddy@141 | 713 if isAuthError(err) { |
| paddy@172 | 714 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 715 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@141 | 716 } else { |
| paddy@172 | 717 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 718 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@141 | 719 } |
| paddy@141 | 720 return |
| paddy@141 | 721 } |
| paddy@133 | 722 var change ClientChange |
| paddy@141 | 723 err = decode(r, &change) |
| paddy@133 | 724 if err != nil { |
| paddy@172 | 725 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/"}) |
| paddy@172 | 726 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@133 | 727 return |
| paddy@133 | 728 } |
| paddy@133 | 729 errs := change.Validate() |
| paddy@133 | 730 for _, err := range errs { |
| paddy@133 | 731 switch err { |
| paddy@133 | 732 case ErrEmptyChange: |
| paddy@172 | 733 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"}) |
| paddy@133 | 734 case ErrClientNameTooShort: |
| paddy@172 | 735 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/name"}) |
| paddy@133 | 736 case ErrClientNameTooLong: |
| paddy@172 | 737 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/name"}) |
| paddy@133 | 738 case ErrClientLogoTooLong: |
| paddy@172 | 739 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/logo"}) |
| paddy@133 | 740 case ErrClientLogoNotURL: |
| paddy@172 | 741 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/logo"}) |
| paddy@133 | 742 case ErrClientWebsiteTooLong: |
| paddy@172 | 743 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/website"}) |
| paddy@133 | 744 case ErrClientWebsiteNotURL: |
| paddy@172 | 745 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/website"}) |
| paddy@133 | 746 default: |
| paddy@133 | 747 log.Println("Unrecognised error from client change validation:", err) |
| paddy@133 | 748 } |
| paddy@133 | 749 } |
| paddy@133 | 750 if len(errors) > 0 { |
| paddy@172 | 751 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@133 | 752 return |
| paddy@133 | 753 } |
| paddy@133 | 754 client, err := c.GetClient(id) |
| paddy@133 | 755 if err == ErrClientNotFound { |
| paddy@172 | 756 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) |
| paddy@172 | 757 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@133 | 758 return |
| paddy@133 | 759 } else if err != nil { |
| paddy@133 | 760 log.Println("Error retrieving client:", err) |
| paddy@172 | 761 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 762 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@133 | 763 return |
| paddy@133 | 764 } |
| paddy@141 | 765 if !client.OwnerID.Equal(profile.ID) { |
| paddy@172 | 766 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 767 encode(w, r, http.StatusForbidden, Response{Errors: errors}) |
| paddy@141 | 768 return |
| paddy@141 | 769 } |
| paddy@133 | 770 if change.Secret != nil && client.Type == clientTypeConfidential { |
| paddy@133 | 771 secret := make([]byte, 32) |
| paddy@133 | 772 _, err = rand.Read(secret) |
| paddy@133 | 773 if err != nil { |
| paddy@133 | 774 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@133 | 775 return |
| paddy@133 | 776 } |
| paddy@133 | 777 newSecret := hex.EncodeToString(secret) |
| paddy@133 | 778 change.Secret = &newSecret |
| paddy@133 | 779 } |
| paddy@133 | 780 err = c.UpdateClient(id, change) |
| paddy@133 | 781 if err != nil { |
| paddy@133 | 782 log.Println("Error updating client:", err) |
| paddy@172 | 783 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 784 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@133 | 785 return |
| paddy@133 | 786 } |
| paddy@133 | 787 client.ApplyChange(change) |
| paddy@172 | 788 encode(w, r, http.StatusOK, Response{Clients: []Client{client}, Errors: errors}) |
| paddy@133 | 789 return |
| paddy@133 | 790 } |
| paddy@133 | 791 |
| paddy@144 | 792 func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@172 | 793 errors := []RequestError{} |
| paddy@144 | 794 vars := mux.Vars(r) |
| paddy@144 | 795 if _, ok := vars["id"]; !ok { |
| paddy@172 | 796 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) |
| paddy@172 | 797 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@144 | 798 return |
| paddy@144 | 799 } |
| paddy@144 | 800 id, err := uuid.Parse(vars["id"]) |
| paddy@144 | 801 if err != nil { |
| paddy@172 | 802 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) |
| paddy@144 | 803 } |
| paddy@144 | 804 username, password, ok := r.BasicAuth() |
| paddy@144 | 805 if !ok { |
| paddy@172 | 806 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 807 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@144 | 808 return |
| paddy@144 | 809 } |
| paddy@144 | 810 profile, err := authenticate(username, password, c) |
| paddy@144 | 811 if err != nil { |
| paddy@144 | 812 if isAuthError(err) { |
| paddy@172 | 813 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 814 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@144 | 815 } else { |
| paddy@172 | 816 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 817 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@144 | 818 } |
| paddy@144 | 819 return |
| paddy@144 | 820 } |
| paddy@144 | 821 client, err := c.GetClient(id) |
| paddy@144 | 822 if err != nil { |
| paddy@144 | 823 if err == ErrClientNotFound { |
| paddy@172 | 824 errors = append(errors, RequestError{Slug: RequestErrNotFound}) |
| paddy@172 | 825 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@144 | 826 return |
| paddy@144 | 827 } |
| paddy@144 | 828 log.Println("Error retrieving client:", err) |
| paddy@172 | 829 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 830 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@144 | 831 return |
| paddy@144 | 832 } |
| paddy@144 | 833 if !client.OwnerID.Equal(profile.ID) { |
| paddy@172 | 834 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 835 encode(w, r, http.StatusForbidden, Response{Errors: errors}) |
| paddy@144 | 836 return |
| paddy@144 | 837 } |
| paddy@151 | 838 deleted := true |
| paddy@151 | 839 change := ClientChange{Deleted: &deleted} |
| paddy@151 | 840 err = c.UpdateClient(id, change) |
| paddy@144 | 841 if err != nil { |
| paddy@144 | 842 if err == ErrClientNotFound { |
| paddy@172 | 843 errors = append(errors, RequestError{Slug: RequestErrNotFound}) |
| paddy@172 | 844 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@144 | 845 return |
| paddy@144 | 846 } |
| paddy@144 | 847 log.Println("Error deleting client:", err) |
| paddy@172 | 848 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 849 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@144 | 850 return |
| paddy@144 | 851 } |
| paddy@172 | 852 encode(w, r, http.StatusOK, Response{Errors: errors}) |
| paddy@164 | 853 go cleanUpAfterClientDeletion(id, c) |
| paddy@144 | 854 return |
| paddy@144 | 855 } |
| paddy@144 | 856 |
| paddy@137 | 857 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@137 | 858 type addEndpointReq struct { |
| paddy@137 | 859 Endpoints []string `json:"endpoints"` |
| paddy@137 | 860 } |
| paddy@172 | 861 errors := []RequestError{} |
| paddy@137 | 862 vars := mux.Vars(r) |
| paddy@137 | 863 if vars["id"] == "" { |
| paddy@172 | 864 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) |
| paddy@172 | 865 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@137 | 866 return |
| paddy@137 | 867 } |
| paddy@137 | 868 id, err := uuid.Parse(vars["id"]) |
| paddy@137 | 869 if err != nil { |
| paddy@172 | 870 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"}) |
| paddy@172 | 871 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@137 | 872 return |
| paddy@137 | 873 } |
| paddy@142 | 874 username, password, ok := r.BasicAuth() |
| paddy@142 | 875 if !ok { |
| paddy@172 | 876 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 877 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@142 | 878 return |
| paddy@142 | 879 } |
| paddy@142 | 880 profile, err := authenticate(username, password, c) |
| paddy@142 | 881 if err != nil { |
| paddy@142 | 882 if isAuthError(err) { |
| paddy@172 | 883 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 884 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@142 | 885 } else { |
| paddy@172 | 886 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 887 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@142 | 888 } |
| paddy@142 | 889 return |
| paddy@142 | 890 } |
| paddy@143 | 891 client, err := c.GetClient(id) |
| paddy@137 | 892 if err != nil { |
| paddy@137 | 893 if err == ErrClientNotFound { |
| paddy@172 | 894 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) |
| paddy@172 | 895 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@137 | 896 return |
| paddy@137 | 897 } |
| paddy@172 | 898 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 899 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@137 | 900 return |
| paddy@137 | 901 } |
| paddy@142 | 902 if !client.OwnerID.Equal(profile.ID) { |
| paddy@172 | 903 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 904 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@142 | 905 return |
| paddy@142 | 906 } |
| paddy@137 | 907 var req addEndpointReq |
| paddy@137 | 908 decoder := json.NewDecoder(r.Body) |
| paddy@137 | 909 err = decoder.Decode(&req) |
| paddy@137 | 910 if err != nil { |
| paddy@137 | 911 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@137 | 912 return |
| paddy@137 | 913 } |
| paddy@137 | 914 if len(req.Endpoints) < 1 { |
| paddy@172 | 915 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/endpoints"}) |
| paddy@172 | 916 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@137 | 917 return |
| paddy@137 | 918 } |
| paddy@137 | 919 endpoints := []Endpoint{} |
| paddy@137 | 920 for pos, u := range req.Endpoints { |
| paddy@137 | 921 if parsed, err := url.Parse(u); err != nil { |
| paddy@172 | 922 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@137 | 923 continue |
| paddy@137 | 924 } else if !parsed.IsAbs() { |
| paddy@172 | 925 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)}) |
| paddy@137 | 926 continue |
| paddy@137 | 927 } |
| paddy@137 | 928 e := Endpoint{ |
| paddy@137 | 929 ID: uuid.NewID(), |
| paddy@137 | 930 ClientID: id, |
| paddy@137 | 931 URI: u, |
| paddy@137 | 932 Added: time.Now(), |
| paddy@137 | 933 } |
| paddy@137 | 934 endpoints = append(endpoints, e) |
| paddy@137 | 935 } |
| paddy@137 | 936 if len(errors) > 0 { |
| paddy@172 | 937 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@137 | 938 return |
| paddy@137 | 939 } |
| paddy@151 | 940 err = c.AddEndpoints(endpoints) |
| paddy@137 | 941 if err != nil { |
| paddy@137 | 942 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@137 | 943 return |
| paddy@137 | 944 } |
| paddy@172 | 945 resp := Response{ |
| paddy@137 | 946 Errors: errors, |
| paddy@137 | 947 Endpoints: endpoints, |
| paddy@137 | 948 } |
| paddy@137 | 949 encode(w, r, http.StatusCreated, resp) |
| paddy@137 | 950 } |
| paddy@137 | 951 |
| paddy@138 | 952 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@172 | 953 errors := []RequestError{} |
| paddy@138 | 954 vars := mux.Vars(r) |
| paddy@138 | 955 clientID, err := uuid.Parse(vars["id"]) |
| paddy@138 | 956 if err != nil { |
| paddy@172 | 957 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "client_id"}) |
| paddy@172 | 958 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@138 | 959 return |
| paddy@138 | 960 } |
| paddy@138 | 961 num := defaultEndpointResponseSize |
| paddy@138 | 962 offset := 0 |
| paddy@138 | 963 numStr := r.URL.Query().Get("num") |
| paddy@138 | 964 offsetStr := r.URL.Query().Get("offset") |
| paddy@138 | 965 if numStr != "" { |
| paddy@138 | 966 num, err = strconv.Atoi(numStr) |
| paddy@138 | 967 if err != nil { |
| paddy@172 | 968 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "num"}) |
| paddy@138 | 969 } |
| paddy@138 | 970 if num > maxEndpointResponseSize { |
| paddy@172 | 971 errors = append(errors, RequestError{Slug: RequestErrOverflow, Param: "num"}) |
| paddy@138 | 972 } |
| paddy@138 | 973 } |
| paddy@138 | 974 if offsetStr != "" { |
| paddy@138 | 975 offset, err = strconv.Atoi(offsetStr) |
| paddy@138 | 976 if err != nil { |
| paddy@172 | 977 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "offset"}) |
| paddy@138 | 978 } |
| paddy@138 | 979 } |
| paddy@138 | 980 if len(errors) > 0 { |
| paddy@172 | 981 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@138 | 982 return |
| paddy@138 | 983 } |
| paddy@138 | 984 endpoints, err := c.ListEndpoints(clientID, num, offset) |
| paddy@138 | 985 if err != nil { |
| paddy@172 | 986 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 987 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@138 | 988 return |
| paddy@138 | 989 } |
| paddy@172 | 990 resp := Response{ |
| paddy@138 | 991 Endpoints: endpoints, |
| paddy@138 | 992 Errors: errors, |
| paddy@138 | 993 } |
| paddy@138 | 994 encode(w, r, http.StatusOK, resp) |
| paddy@138 | 995 } |
| paddy@138 | 996 |
| paddy@143 | 997 func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@172 | 998 errors := []RequestError{} |
| paddy@143 | 999 vars := mux.Vars(r) |
| paddy@143 | 1000 if vars["client_id"] == "" { |
| paddy@172 | 1001 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "client_id"}) |
| paddy@172 | 1002 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@143 | 1003 return |
| paddy@143 | 1004 } |
| paddy@143 | 1005 clientID, err := uuid.Parse(vars["client_id"]) |
| paddy@143 | 1006 if err != nil { |
| paddy@172 | 1007 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "client_id"}) |
| paddy@172 | 1008 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@143 | 1009 return |
| paddy@143 | 1010 } |
| paddy@143 | 1011 if vars["id"] == "" { |
| paddy@172 | 1012 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) |
| paddy@172 | 1013 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@143 | 1014 return |
| paddy@143 | 1015 } |
| paddy@143 | 1016 id, err := uuid.Parse(vars["id"]) |
| paddy@143 | 1017 if err != nil { |
| paddy@172 | 1018 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"}) |
| paddy@172 | 1019 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@143 | 1020 return |
| paddy@143 | 1021 } |
| paddy@143 | 1022 username, password, ok := r.BasicAuth() |
| paddy@143 | 1023 if !ok { |
| paddy@172 | 1024 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 1025 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@143 | 1026 return |
| paddy@143 | 1027 } |
| paddy@143 | 1028 profile, err := authenticate(username, password, c) |
| paddy@143 | 1029 if err != nil { |
| paddy@143 | 1030 if isAuthError(err) { |
| paddy@172 | 1031 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 1032 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@143 | 1033 } else { |
| paddy@172 | 1034 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 1035 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@143 | 1036 } |
| paddy@143 | 1037 return |
| paddy@143 | 1038 } |
| paddy@143 | 1039 client, err := c.GetClient(clientID) |
| paddy@143 | 1040 if err != nil { |
| paddy@143 | 1041 if err == ErrClientNotFound { |
| paddy@172 | 1042 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "client_id"}) |
| paddy@172 | 1043 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@143 | 1044 return |
| paddy@143 | 1045 } |
| paddy@172 | 1046 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 1047 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@143 | 1048 return |
| paddy@143 | 1049 } |
| paddy@143 | 1050 if !client.OwnerID.Equal(profile.ID) { |
| paddy@172 | 1051 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 1052 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@143 | 1053 return |
| paddy@143 | 1054 } |
| paddy@143 | 1055 endpoint, err := c.GetEndpoint(clientID, id) |
| paddy@143 | 1056 if err != nil { |
| paddy@143 | 1057 if err == ErrEndpointNotFound { |
| paddy@172 | 1058 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) |
| paddy@172 | 1059 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@143 | 1060 return |
| paddy@143 | 1061 } |
| paddy@172 | 1062 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 1063 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@143 | 1064 return |
| paddy@143 | 1065 } |
| paddy@143 | 1066 err = c.RemoveEndpoint(clientID, id) |
| paddy@143 | 1067 if err != nil { |
| paddy@143 | 1068 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@143 | 1069 return |
| paddy@143 | 1070 } |
| paddy@172 | 1071 resp := Response{ |
| paddy@143 | 1072 Errors: errors, |
| paddy@143 | 1073 Endpoints: []Endpoint{endpoint}, |
| paddy@143 | 1074 } |
| paddy@143 | 1075 encode(w, r, http.StatusCreated, resp) |
| paddy@143 | 1076 } |
| paddy@143 | 1077 |
| paddy@181 | 1078 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes scopeTypes.Scopes, profileID uuid.ID, valid bool) { |
| paddy@181 | 1079 scopes = scopeTypes.StringsToScopes(strings.Split(r.PostFormValue("scope"), " ")) |
| paddy@121 | 1080 valid = true |
| paddy@121 | 1081 return |
| paddy@121 | 1082 } |
| paddy@124 | 1083 |
| paddy@124 | 1084 func clientCredentialsAuditString(r *http.Request) string { |
| paddy@124 | 1085 return "client_credentials" |
| paddy@124 | 1086 } |