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