auth

Paddy 2015-03-07 Parent:8fad4d66c7ea Child:f1c8e13e1ce6

141:a8e6122bfc1a Go to Latest

auth/client.go

Require authentication to update Clients. Require the Client's owner to supply basic authentication when updating a client.

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@141 641 id, err := uuid.Parse(vars["id"])
paddy@141 642 if err != nil {
paddy@141 643 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
paddy@141 644 }
paddy@141 645 username, password, ok := r.BasicAuth()
paddy@141 646 if !ok {
paddy@141 647 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@141 648 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@141 649 return
paddy@141 650 }
paddy@141 651 profile, err := authenticate(username, password, c)
paddy@141 652 if err != nil {
paddy@141 653 if isAuthError(err) {
paddy@141 654 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@141 655 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@141 656 } else {
paddy@141 657 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@141 658 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@141 659 }
paddy@141 660 return
paddy@141 661 }
paddy@133 662 var change ClientChange
paddy@141 663 err = decode(r, &change)
paddy@133 664 if err != nil {
paddy@133 665 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
paddy@133 666 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@133 667 return
paddy@133 668 }
paddy@133 669 errs := change.Validate()
paddy@133 670 for _, err := range errs {
paddy@133 671 switch err {
paddy@133 672 case ErrEmptyChange:
paddy@133 673 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"})
paddy@133 674 case ErrClientNameTooShort:
paddy@133 675 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
paddy@133 676 case ErrClientNameTooLong:
paddy@133 677 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
paddy@133 678 case ErrClientLogoTooLong:
paddy@133 679 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"})
paddy@133 680 case ErrClientLogoNotURL:
paddy@133 681 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"})
paddy@133 682 case ErrClientWebsiteTooLong:
paddy@133 683 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"})
paddy@133 684 case ErrClientWebsiteNotURL:
paddy@133 685 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"})
paddy@133 686 default:
paddy@133 687 log.Println("Unrecognised error from client change validation:", err)
paddy@133 688 }
paddy@133 689 }
paddy@133 690 if len(errors) > 0 {
paddy@133 691 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@133 692 return
paddy@133 693 }
paddy@133 694 client, err := c.GetClient(id)
paddy@133 695 if err == ErrClientNotFound {
paddy@133 696 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
paddy@133 697 encode(w, r, http.StatusNotFound, response{Errors: errors})
paddy@133 698 return
paddy@133 699 } else if err != nil {
paddy@133 700 log.Println("Error retrieving 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@141 705 if !client.OwnerID.Equal(profile.ID) {
paddy@141 706 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@141 707 encode(w, r, http.StatusForbidden, response{Errors: errors})
paddy@141 708 return
paddy@141 709 }
paddy@133 710 if change.Secret != nil && client.Type == clientTypeConfidential {
paddy@133 711 secret := make([]byte, 32)
paddy@133 712 _, err = rand.Read(secret)
paddy@133 713 if err != nil {
paddy@133 714 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@133 715 return
paddy@133 716 }
paddy@133 717 newSecret := hex.EncodeToString(secret)
paddy@133 718 change.Secret = &newSecret
paddy@133 719 }
paddy@133 720 err = c.UpdateClient(id, change)
paddy@133 721 if err != nil {
paddy@133 722 log.Println("Error updating client:", err)
paddy@133 723 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@133 724 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@133 725 return
paddy@133 726 }
paddy@133 727 client.ApplyChange(change)
paddy@133 728 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
paddy@133 729 return
paddy@133 730 }
paddy@133 731
paddy@137 732 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
paddy@137 733 type addEndpointReq struct {
paddy@137 734 Endpoints []string `json:"endpoints"`
paddy@137 735 }
paddy@137 736 errors := []requestError{}
paddy@137 737 vars := mux.Vars(r)
paddy@137 738 if vars["id"] == "" {
paddy@137 739 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@137 740 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@137 741 return
paddy@137 742 }
paddy@137 743 id, err := uuid.Parse(vars["id"])
paddy@137 744 if err != nil {
paddy@137 745 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
paddy@137 746 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@137 747 return
paddy@137 748 }
paddy@137 749 _, err = c.GetClient(id)
paddy@137 750 if err != nil {
paddy@137 751 if err == ErrClientNotFound {
paddy@137 752 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
paddy@137 753 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@137 754 return
paddy@137 755 }
paddy@137 756 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@137 757 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@137 758 return
paddy@137 759 }
paddy@137 760 var req addEndpointReq
paddy@137 761 decoder := json.NewDecoder(r.Body)
paddy@137 762 err = decoder.Decode(&req)
paddy@137 763 if err != nil {
paddy@137 764 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@137 765 return
paddy@137 766 }
paddy@137 767 if len(req.Endpoints) < 1 {
paddy@137 768 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"})
paddy@137 769 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@137 770 return
paddy@137 771 }
paddy@137 772 endpoints := []Endpoint{}
paddy@137 773 for pos, u := range req.Endpoints {
paddy@137 774 if parsed, err := url.Parse(u); err != nil {
paddy@137 775 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
paddy@137 776 continue
paddy@137 777 } else if !parsed.IsAbs() {
paddy@137 778 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
paddy@137 779 continue
paddy@137 780 }
paddy@137 781 e := Endpoint{
paddy@137 782 ID: uuid.NewID(),
paddy@137 783 ClientID: id,
paddy@137 784 URI: u,
paddy@137 785 Added: time.Now(),
paddy@137 786 }
paddy@137 787 endpoints = append(endpoints, e)
paddy@137 788 }
paddy@137 789 if len(errors) > 0 {
paddy@137 790 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@137 791 return
paddy@137 792 }
paddy@137 793 err = c.AddEndpoints(id, endpoints)
paddy@137 794 if err != nil {
paddy@137 795 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@137 796 return
paddy@137 797 }
paddy@137 798 resp := response{
paddy@137 799 Errors: errors,
paddy@137 800 Endpoints: endpoints,
paddy@137 801 }
paddy@137 802 encode(w, r, http.StatusCreated, resp)
paddy@137 803 }
paddy@137 804
paddy@138 805 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
paddy@138 806 errors := []requestError{}
paddy@138 807 vars := mux.Vars(r)
paddy@138 808 clientID, err := uuid.Parse(vars["id"])
paddy@138 809 if err != nil {
paddy@138 810 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
paddy@138 811 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@138 812 return
paddy@138 813 }
paddy@138 814 num := defaultEndpointResponseSize
paddy@138 815 offset := 0
paddy@138 816 numStr := r.URL.Query().Get("num")
paddy@138 817 offsetStr := r.URL.Query().Get("offset")
paddy@138 818 if numStr != "" {
paddy@138 819 num, err = strconv.Atoi(numStr)
paddy@138 820 if err != nil {
paddy@138 821 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
paddy@138 822 }
paddy@138 823 if num > maxEndpointResponseSize {
paddy@138 824 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
paddy@138 825 }
paddy@138 826 }
paddy@138 827 if offsetStr != "" {
paddy@138 828 offset, err = strconv.Atoi(offsetStr)
paddy@138 829 if err != nil {
paddy@138 830 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
paddy@138 831 }
paddy@138 832 }
paddy@138 833 if len(errors) > 0 {
paddy@138 834 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@138 835 return
paddy@138 836 }
paddy@138 837 endpoints, err := c.ListEndpoints(clientID, num, offset)
paddy@138 838 if err != nil {
paddy@138 839 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@138 840 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@138 841 return
paddy@138 842 }
paddy@138 843 resp := response{
paddy@138 844 Endpoints: endpoints,
paddy@138 845 Errors: errors,
paddy@138 846 }
paddy@138 847 encode(w, r, http.StatusOK, resp)
paddy@138 848 }
paddy@138 849
paddy@135 850 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
paddy@135 851 scopes = strings.Split(r.PostFormValue("scope"), " ")
paddy@121 852 valid = true
paddy@121 853 return
paddy@121 854 }
paddy@124 855
paddy@124 856 func clientCredentialsAuditString(r *http.Request) string {
paddy@124 857 return "client_credentials"
paddy@124 858 }