auth

Paddy 2015-03-07 Parent:026adb0c7fc4 Child:a8e6122bfc1a

140:8fad4d66c7ea Go to Latest

auth/client.go

Return client Secrets when listing clients with basic auth. If the request to list clients is sent with basic auth containing the login and password for the owner of the client, its secret is not removed from the response before sending it.

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