auth

Paddy 2015-05-15 Parent:cf1aef6eb81f Child:8ecb60d29b0d

168:581c60f8dd23 Go to Latest

auth/client.go

Switch to a JWT approach. We're going to use a JWT as our access tokens (as discussed in &yet's excellent post https://blog.andyet.com/2015/05/12/micro-services-user-info-and-auth and my ensuing conversation with Fritzy). The benefit of this approach is that we can do authentication and even some authorization without touching the database at all. The drawback is that we can no longer revoke access tokens, only the refresh tokens that grant the access tokens. We need a new config variable to set our private key, used to sign the JWT. We get to remove our token handlers, as we no longer can revoke tokens, so there's no purpose in getting information about it or listing them. Our tokenStore revokeToken gets to be simplified, as it will only ever be used for refresh tokens now. We also updated our postgres and memstore implementations. We added a helper method for generating the signed "access token" (our JWT) and started using it in the places where we're creating a Token. We get to remove the `revoked` SQL column for the tokens table, and rename the `refresh_revoked` column to just be `revoked`. We shortened our access token expiration to 15 minutes instead of an hour, to deal with the token not being revokable.

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