auth

Paddy 2015-03-03 Parent:bc842183181d Child:f59559b33c76

135:d30a3a12d387 Go to Latest

auth/client.go

Attach our Scope type to AuthCodes and Tokens. When obtaining an AuthorizationCode or Token, attach a slice of strings, each one a Scope ID, instead of just attaching the encoded string the user passes in. This will allow us to change our Scope encoding down the line, and is more conceptually faithful. Also, if an authorization request is made with an invalid scope, return the invalid_scope error.

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