auth

Paddy 2015-01-24 Parent:6c755b23ec80 Child:bc842183181d

131:f474ce964dcf Go to Latest

auth/client.go

Implement handlers for retrieving clients. Create a GetClientHandler and ListClientsHandler for retrieving details about a client. Currently, we're not returning the client secret for these clients. We're also not doing any auth. We may want to restrict auth to the owner of the clients, and return secrets only when auth'd, and maybe even only when a special header is included. Alternatively, we could only return the secret when retrieving a single client. Still unsure how I want to handle that.

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@41 12 "time"
paddy@31 13
paddy@116 14 "github.com/PuerkitoBio/purell"
paddy@116 15 "github.com/gorilla/mux"
paddy@116 16
paddy@107 17 "code.secondbit.org/uuid.hg"
paddy@0 18 )
paddy@0 19
paddy@121 20 func init() {
paddy@121 21 RegisterGrantType("client_credentials", GrantType{
paddy@121 22 Validate: clientCredentialsValidate,
paddy@121 23 Invalidate: nil,
paddy@121 24 IssuesRefresh: true,
paddy@121 25 ReturnToken: RenderJSONToken,
paddy@123 26 AllowsPublic: false,
paddy@124 27 AuditString: clientCredentialsAuditString,
paddy@121 28 })
paddy@121 29 }
paddy@121 30
paddy@31 31 var (
paddy@57 32 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first.
paddy@57 33 ErrNoClientStore = errors.New("no clientStore was specified for the Context")
paddy@57 34 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore.
paddy@57 35 ErrClientNotFound = errors.New("client not found in clientStore")
paddy@57 36 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with
paddy@57 37 // the same ID already exists in the clientStore.
paddy@57 38 ErrClientAlreadyExists = errors.New("client already exists in clientStore")
paddy@41 39
paddy@57 40 // ErrEmptyChange is returned when a Change has all its properties set to nil.
paddy@57 41 ErrEmptyChange = errors.New("change must have at least one property set")
paddy@57 42 // ErrClientNameTooShort is returned when a Client's Name property is too short.
paddy@57 43 ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
paddy@57 44 // ErrClientNameTooLong is returned when a Client's Name property is too long.
paddy@57 45 ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
paddy@57 46 // ErrClientLogoTooLong is returned when a Client's Logo property is too long.
paddy@57 47 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
paddy@57 48 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL.
paddy@57 49 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
paddy@57 50 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long.
paddy@49 51 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
paddy@57 52 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL.
paddy@57 53 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
paddy@116 54 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL.
paddy@116 55 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL")
paddy@31 56 )
paddy@31 57
paddy@115 58 const (
paddy@131 59 clientTypePublic = "public"
paddy@131 60 clientTypeConfidential = "confidential"
paddy@131 61 minClientNameLen = 2
paddy@131 62 maxClientNameLen = 24
paddy@131 63 defaultClientResponseSize = 20
paddy@131 64 maxClientResponseSize = 50
paddy@130 65
paddy@130 66 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery
paddy@115 67 )
paddy@115 68
paddy@25 69 // Client represents a client that grants access
paddy@25 70 // to the auth server, exchanging grants for tokens,
paddy@25 71 // and tokens for access.
paddy@0 72 type Client struct {
paddy@116 73 ID uuid.ID `json:"id,omitempty"`
paddy@116 74 Secret string `json:"secret,omitempty"`
paddy@116 75 OwnerID uuid.ID `json:"owner_id,omitempty"`
paddy@116 76 Name string `json:"name,omitempty"`
paddy@116 77 Logo string `json:"logo,omitempty"`
paddy@116 78 Website string `json:"website,omitempty"`
paddy@116 79 Type string `json:"type,omitempty"`
paddy@0 80 }
paddy@0 81
paddy@57 82 // ApplyChange applies the properties of the passed
paddy@57 83 // ClientChange to the Client object it is called on.
paddy@39 84 func (c *Client) ApplyChange(change ClientChange) {
paddy@39 85 if change.Secret != nil {
paddy@39 86 c.Secret = *change.Secret
paddy@39 87 }
paddy@39 88 if change.OwnerID != nil {
paddy@39 89 c.OwnerID = change.OwnerID
paddy@39 90 }
paddy@39 91 if change.Name != nil {
paddy@39 92 c.Name = *change.Name
paddy@39 93 }
paddy@39 94 if change.Logo != nil {
paddy@39 95 c.Logo = *change.Logo
paddy@39 96 }
paddy@39 97 if change.Website != nil {
paddy@39 98 c.Website = *change.Website
paddy@39 99 }
paddy@39 100 }
paddy@39 101
paddy@57 102 // ClientChange represents a bundle of options for
paddy@57 103 // updating a Client's mutable data.
paddy@31 104 type ClientChange struct {
paddy@41 105 Secret *string
paddy@41 106 OwnerID uuid.ID
paddy@41 107 Name *string
paddy@41 108 Logo *string
paddy@41 109 Website *string
paddy@31 110 }
paddy@31 111
paddy@57 112 // Validate checks the ClientChange it is called on
paddy@57 113 // and asserts its internal validity, or lack thereof.
paddy@39 114 func (c ClientChange) Validate() error {
paddy@42 115 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
paddy@42 116 return ErrEmptyChange
paddy@42 117 }
paddy@41 118 if c.Name != nil && len(*c.Name) < 2 {
paddy@41 119 return ErrClientNameTooShort
paddy@41 120 }
paddy@41 121 if c.Name != nil && len(*c.Name) > 32 {
paddy@41 122 return ErrClientNameTooLong
paddy@41 123 }
paddy@42 124 if c.Logo != nil && *c.Logo != "" {
paddy@42 125 if len(*c.Logo) > 1024 {
paddy@42 126 return ErrClientLogoTooLong
paddy@42 127 }
paddy@42 128 u, err := url.Parse(*c.Logo)
paddy@42 129 if err != nil || !u.IsAbs() {
paddy@42 130 return ErrClientLogoNotURL
paddy@42 131 }
paddy@41 132 }
paddy@42 133 if c.Website != nil && *c.Website != "" {
paddy@42 134 if len(*c.Website) > 140 {
paddy@42 135 return ErrClientWebsiteTooLong
paddy@42 136 }
paddy@42 137 u, err := url.Parse(*c.Website)
paddy@42 138 if err != nil || !u.IsAbs() {
paddy@42 139 return ErrClientWebsiteNotURL
paddy@42 140 }
paddy@41 141 }
paddy@39 142 return nil
paddy@39 143 }
paddy@39 144
paddy@123 145 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) {
paddy@85 146 enc := json.NewEncoder(w)
paddy@85 147 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
paddy@85 148 if !fromAuthHeader {
paddy@85 149 clientIDStr = r.PostFormValue("client_id")
paddy@85 150 }
paddy@123 151 if clientIDStr == "" {
paddy@85 152 w.WriteHeader(http.StatusUnauthorized)
paddy@85 153 if fromAuthHeader {
paddy@85 154 w.Header().Set("WWW-Authenticate", "Basic")
paddy@85 155 }
paddy@85 156 renderJSONError(enc, "invalid_client")
paddy@123 157 return nil, "", false
paddy@123 158 }
paddy@129 159 if !allowPublic && !fromAuthHeader {
paddy@129 160 w.WriteHeader(http.StatusBadRequest)
paddy@129 161 renderJSONError(enc, "unauthorized_client")
paddy@129 162 return nil, "", false
paddy@129 163 }
paddy@123 164 clientID, err := uuid.Parse(clientIDStr)
paddy@123 165 if err != nil {
paddy@123 166 log.Println("Error decoding client ID:", err)
paddy@123 167 w.WriteHeader(http.StatusUnauthorized)
paddy@123 168 if fromAuthHeader {
paddy@123 169 w.Header().Set("WWW-Authenticate", "Basic")
paddy@123 170 }
paddy@123 171 renderJSONError(enc, "invalid_client")
paddy@123 172 return nil, "", false
paddy@123 173 }
paddy@123 174 return clientID, clientSecret, true
paddy@123 175 }
paddy@123 176
paddy@123 177 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
paddy@123 178 enc := json.NewEncoder(w)
paddy@123 179 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic)
paddy@123 180 if !ok {
paddy@85 181 return nil, false
paddy@85 182 }
paddy@123 183 _, _, fromAuthHeader := r.BasicAuth()
paddy@85 184 client, err := context.GetClient(clientID)
paddy@85 185 if err == ErrClientNotFound {
paddy@85 186 w.WriteHeader(http.StatusUnauthorized)
paddy@85 187 if fromAuthHeader {
paddy@85 188 w.Header().Set("WWW-Authenticate", "Basic")
paddy@85 189 }
paddy@85 190 renderJSONError(enc, "invalid_client")
paddy@85 191 return nil, false
paddy@85 192 } else if err != nil {
paddy@85 193 w.WriteHeader(http.StatusInternalServerError)
paddy@85 194 renderJSONError(enc, "server_error")
paddy@85 195 return nil, false
paddy@85 196 }
paddy@113 197 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
paddy@85 198 w.WriteHeader(http.StatusUnauthorized)
paddy@85 199 if fromAuthHeader {
paddy@85 200 w.Header().Set("WWW-Authenticate", "Basic")
paddy@85 201 }
paddy@85 202 renderJSONError(enc, "invalid_client")
paddy@85 203 return nil, false
paddy@85 204 }
paddy@85 205 return clientID, true
paddy@85 206 }
paddy@85 207
paddy@57 208 // Endpoint represents a single URI that a Client
paddy@57 209 // controls. Users will be redirected to these URIs
paddy@57 210 // following successful authorization grants and
paddy@57 211 // exchanges for access tokens.
paddy@41 212 type Endpoint struct {
paddy@116 213 ID uuid.ID `json:"id,omitempty"`
paddy@116 214 ClientID uuid.ID `json:"client_id,omitempty"`
paddy@116 215 URI string `json:"uri,omitempty"`
paddy@116 216 NormalizedURI string `json:"-"`
paddy@116 217 Added time.Time `json:"added,omitempty"`
paddy@116 218 }
paddy@116 219
paddy@116 220 func normalizeURIString(in string) (string, error) {
paddy@130 221 n, err := purell.NormalizeURLString(in, normalizeFlags)
paddy@116 222 if err != nil {
paddy@116 223 log.Println(err)
paddy@116 224 return in, ErrEndpointURINotURL
paddy@116 225 }
paddy@116 226 return n, nil
paddy@116 227 }
paddy@116 228
paddy@116 229 func normalizeURI(in *url.URL) string {
paddy@130 230 return purell.NormalizeURL(in, normalizeFlags)
paddy@41 231 }
paddy@41 232
paddy@41 233 type sortedEndpoints []Endpoint
paddy@41 234
paddy@41 235 func (s sortedEndpoints) Len() int {
paddy@41 236 return len(s)
paddy@41 237 }
paddy@41 238
paddy@41 239 func (s sortedEndpoints) Less(i, j int) bool {
paddy@41 240 return s[i].Added.Before(s[j].Added)
paddy@41 241 }
paddy@41 242
paddy@41 243 func (s sortedEndpoints) Swap(i, j int) {
paddy@41 244 s[i], s[j] = s[j], s[i]
paddy@41 245 }
paddy@41 246
paddy@57 247 type clientStore interface {
paddy@57 248 getClient(id uuid.ID) (Client, error)
paddy@57 249 saveClient(client Client) error
paddy@57 250 updateClient(id uuid.ID, change ClientChange) error
paddy@57 251 deleteClient(id uuid.ID) error
paddy@57 252 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
paddy@41 253
paddy@115 254 addEndpoints(client uuid.ID, endpoint []Endpoint) error
paddy@57 255 removeEndpoint(client, endpoint uuid.ID) error
paddy@58 256 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
paddy@57 257 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
paddy@57 258 countEndpoints(client uuid.ID) (int64, error)
paddy@0 259 }
paddy@31 260
paddy@57 261 func (m *memstore) getClient(id uuid.ID) (Client, error) {
paddy@31 262 m.clientLock.RLock()
paddy@31 263 defer m.clientLock.RUnlock()
paddy@31 264 c, ok := m.clients[id.String()]
paddy@31 265 if !ok {
paddy@31 266 return Client{}, ErrClientNotFound
paddy@31 267 }
paddy@31 268 return c, nil
paddy@31 269 }
paddy@31 270
paddy@57 271 func (m *memstore) saveClient(client Client) error {
paddy@31 272 m.clientLock.Lock()
paddy@31 273 defer m.clientLock.Unlock()
paddy@31 274 if _, ok := m.clients[client.ID.String()]; ok {
paddy@31 275 return ErrClientAlreadyExists
paddy@31 276 }
paddy@31 277 m.clients[client.ID.String()] = client
paddy@31 278 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
paddy@31 279 return nil
paddy@31 280 }
paddy@31 281
paddy@57 282 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
paddy@39 283 m.clientLock.Lock()
paddy@39 284 defer m.clientLock.Unlock()
paddy@39 285 c, ok := m.clients[id.String()]
paddy@39 286 if !ok {
paddy@39 287 return ErrClientNotFound
paddy@39 288 }
paddy@39 289 c.ApplyChange(change)
paddy@39 290 m.clients[id.String()] = c
paddy@31 291 return nil
paddy@31 292 }
paddy@31 293
paddy@57 294 func (m *memstore) deleteClient(id uuid.ID) error {
paddy@57 295 client, err := m.getClient(id)
paddy@31 296 if err != nil {
paddy@31 297 return err
paddy@31 298 }
paddy@31 299 m.clientLock.Lock()
paddy@31 300 defer m.clientLock.Unlock()
paddy@31 301 delete(m.clients, id.String())
paddy@31 302 pos := -1
paddy@31 303 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
paddy@31 304 if item.Equal(id) {
paddy@31 305 pos = p
paddy@31 306 break
paddy@31 307 }
paddy@31 308 }
paddy@31 309 if pos >= 0 {
paddy@31 310 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
paddy@31 311 }
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@31 317 if len(ids) > num+offset {
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@31 328 return []Client{}, err
paddy@31 329 }
paddy@31 330 clients = append(clients, client)
paddy@31 331 }
paddy@31 332 return clients, nil
paddy@31 333 }
paddy@41 334
paddy@115 335 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error {
paddy@41 336 m.endpointLock.Lock()
paddy@41 337 defer m.endpointLock.Unlock()
paddy@115 338 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...)
paddy@41 339 return nil
paddy@41 340 }
paddy@41 341
paddy@57 342 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
paddy@41 343 m.endpointLock.Lock()
paddy@41 344 defer m.endpointLock.Unlock()
paddy@41 345 pos := -1
paddy@41 346 for p, item := range m.endpoints[client.String()] {
paddy@41 347 if item.ID.Equal(endpoint) {
paddy@41 348 pos = p
paddy@41 349 break
paddy@41 350 }
paddy@41 351 }
paddy@41 352 if pos >= 0 {
paddy@41 353 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
paddy@41 354 }
paddy@41 355 return nil
paddy@41 356 }
paddy@41 357
paddy@58 358 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
paddy@41 359 m.endpointLock.RLock()
paddy@41 360 defer m.endpointLock.RUnlock()
paddy@41 361 for _, candidate := range m.endpoints[client.String()] {
paddy@116 362 if endpoint == candidate.NormalizedURI {
paddy@41 363 return true, nil
paddy@41 364 }
paddy@41 365 }
paddy@41 366 return false, nil
paddy@41 367 }
paddy@41 368
paddy@57 369 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
paddy@41 370 m.endpointLock.RLock()
paddy@41 371 defer m.endpointLock.RUnlock()
paddy@41 372 return m.endpoints[client.String()], nil
paddy@41 373 }
paddy@54 374
paddy@57 375 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
paddy@54 376 m.endpointLock.RLock()
paddy@54 377 defer m.endpointLock.RUnlock()
paddy@54 378 return int64(len(m.endpoints[client.String()])), nil
paddy@54 379 }
paddy@108 380
paddy@108 381 type newClientReq struct {
paddy@108 382 Name string `json:"name"`
paddy@108 383 Logo string `json:"logo"`
paddy@108 384 Website string `json:"website"`
paddy@108 385 Type string `json:"type"`
paddy@108 386 Endpoints []string `json:"endpoints"`
paddy@108 387 }
paddy@108 388
paddy@108 389 func RegisterClientHandlers(r *mux.Router, context Context) {
paddy@108 390 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
paddy@131 391 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET")
paddy@131 392 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET")
paddy@128 393 // BUG(paddy): We need to implement a handler to update a client.
paddy@128 394 // BUG(paddy): We need to implement a handler to delete a client. Also, what should that do with the grants and tokens belonging to that client?
paddy@128 395 // BUG(paddy): We need to implement a handler to add an endpoint to a client.
paddy@128 396 // BUG(paddy): We need to implement a handler to remove an endpoint from a client.
paddy@128 397 // BUG(paddy): We need to implement a handler to list endpoints.
paddy@108 398 }
paddy@108 399
paddy@108 400 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
paddy@115 401 errors := []requestError{}
paddy@108 402 username, password, ok := r.BasicAuth()
paddy@108 403 if !ok {
paddy@115 404 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@115 405 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@108 406 return
paddy@108 407 }
paddy@108 408 profile, err := authenticate(username, password, c)
paddy@108 409 if err != nil {
paddy@115 410 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@115 411 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@108 412 return
paddy@108 413 }
paddy@108 414 var req newClientReq
paddy@108 415 decoder := json.NewDecoder(r.Body)
paddy@108 416 err = decoder.Decode(&req)
paddy@108 417 if err != nil {
paddy@108 418 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@108 419 return
paddy@108 420 }
paddy@116 421 if req.Type == "" {
paddy@116 422 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
paddy@116 423 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
paddy@115 424 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
paddy@116 425 }
paddy@116 426 if req.Name == "" {
paddy@116 427 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
paddy@116 428 } else if len(req.Name) < minClientNameLen {
paddy@116 429 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
paddy@116 430 } else if len(req.Name) > maxClientNameLen {
paddy@116 431 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
paddy@116 432 }
paddy@116 433 if len(errors) > 0 {
paddy@115 434 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@108 435 return
paddy@108 436 }
paddy@108 437 client := Client{
paddy@108 438 ID: uuid.NewID(),
paddy@108 439 OwnerID: profile.ID,
paddy@108 440 Name: req.Name,
paddy@108 441 Logo: req.Logo,
paddy@108 442 Website: req.Website,
paddy@108 443 Type: req.Type,
paddy@108 444 }
paddy@118 445 if client.Type == clientTypeConfidential {
paddy@115 446 secret := make([]byte, 32)
paddy@115 447 _, err = rand.Read(secret)
paddy@115 448 if err != nil {
paddy@115 449 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@115 450 return
paddy@115 451 }
paddy@115 452 client.Secret = hex.EncodeToString(secret)
paddy@115 453 }
paddy@108 454 err = c.SaveClient(client)
paddy@108 455 if err != nil {
paddy@115 456 if err == ErrClientAlreadyExists {
paddy@115 457 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
paddy@115 458 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@115 459 return
paddy@115 460 }
paddy@115 461 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@108 462 return
paddy@108 463 }
paddy@108 464 endpoints := []Endpoint{}
paddy@115 465 for pos, u := range req.Endpoints {
paddy@108 466 uri, err := url.Parse(u)
paddy@108 467 if err != nil {
paddy@115 468 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
paddy@108 469 continue
paddy@108 470 }
paddy@116 471 if !uri.IsAbs() {
paddy@116 472 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
paddy@116 473 continue
paddy@116 474 }
paddy@108 475 endpoint := Endpoint{
paddy@108 476 ID: uuid.NewID(),
paddy@108 477 ClientID: client.ID,
paddy@116 478 URI: uri.String(),
paddy@108 479 Added: time.Now(),
paddy@108 480 }
paddy@108 481 endpoints = append(endpoints, endpoint)
paddy@108 482 }
paddy@115 483 err = c.AddEndpoints(client.ID, endpoints)
paddy@115 484 if err != nil {
paddy@115 485 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@115 486 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
paddy@115 487 return
paddy@115 488 }
paddy@108 489 resp := response{
paddy@108 490 Clients: []Client{client},
paddy@108 491 Endpoints: endpoints,
paddy@116 492 Errors: errors,
paddy@108 493 }
paddy@108 494 encode(w, r, http.StatusCreated, resp)
paddy@108 495 }
paddy@121 496
paddy@131 497 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
paddy@131 498 errors := []requestError{}
paddy@131 499 vars := mux.Vars(r)
paddy@131 500 if vars["id"] == "" {
paddy@131 501 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@131 502 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@131 503 return
paddy@131 504 }
paddy@131 505 id, err := uuid.Parse(vars["id"])
paddy@131 506 if err != nil {
paddy@131 507 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
paddy@131 508 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@131 509 return
paddy@131 510 }
paddy@131 511 client, err := c.GetClient(id)
paddy@131 512 if err != nil {
paddy@131 513 if err == ErrClientNotFound {
paddy@131 514 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
paddy@131 515 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@131 516 return
paddy@131 517 }
paddy@131 518 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@131 519 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@131 520 return
paddy@131 521 }
paddy@131 522 client.Secret = ""
paddy@131 523 // BUG(paddy): How should auth be handled for retrieving clients?
paddy@131 524 resp := response{
paddy@131 525 Clients: []Client{client},
paddy@131 526 Errors: errors,
paddy@131 527 }
paddy@131 528 encode(w, r, http.StatusOK, resp)
paddy@131 529 }
paddy@131 530
paddy@131 531 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
paddy@131 532 errors := []requestError{}
paddy@131 533 var err error
paddy@131 534 // BUG(paddy): If ids are provided in query params, retrieve only those clients
paddy@131 535 // BUG(paddy): We should have auth when listing clients
paddy@131 536 num := defaultClientResponseSize
paddy@131 537 offset := 0
paddy@131 538 ownerIDStr := r.URL.Query().Get("owner_id")
paddy@131 539 numStr := r.URL.Query().Get("num")
paddy@131 540 offsetStr := r.URL.Query().Get("offset")
paddy@131 541 if numStr != "" {
paddy@131 542 num, err = strconv.Atoi(numStr)
paddy@131 543 if err != nil {
paddy@131 544 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
paddy@131 545 }
paddy@131 546 if num > maxClientResponseSize {
paddy@131 547 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
paddy@131 548 }
paddy@131 549 }
paddy@131 550 if offsetStr != "" {
paddy@131 551 offset, err = strconv.Atoi(offsetStr)
paddy@131 552 if err != nil {
paddy@131 553 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
paddy@131 554 }
paddy@131 555 }
paddy@131 556 if ownerIDStr == "" {
paddy@131 557 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
paddy@131 558 }
paddy@131 559 if len(errors) > 0 {
paddy@131 560 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@131 561 return
paddy@131 562 }
paddy@131 563 ownerID, err := uuid.Parse(ownerIDStr)
paddy@131 564 if err != nil {
paddy@131 565 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
paddy@131 566 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@131 567 return
paddy@131 568 }
paddy@131 569 clients, err := c.ListClientsByOwner(ownerID, num, offset)
paddy@131 570 if err != nil {
paddy@131 571 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@131 572 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@131 573 return
paddy@131 574 }
paddy@131 575 for pos, client := range clients {
paddy@131 576 client.Secret = ""
paddy@131 577 clients[pos] = client
paddy@131 578 }
paddy@131 579 resp := response{
paddy@131 580 Clients: clients,
paddy@131 581 Errors: errors,
paddy@131 582 }
paddy@131 583 encode(w, r, http.StatusOK, resp)
paddy@131 584 }
paddy@131 585
paddy@121 586 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
paddy@121 587 scope = r.PostFormValue("scope")
paddy@121 588 valid = true
paddy@121 589 return
paddy@121 590 }
paddy@124 591
paddy@124 592 func clientCredentialsAuditString(r *http.Request) string {
paddy@124 593 return "client_credentials"
paddy@124 594 }