auth

Paddy 2015-02-20 Parent:bc842183181d Child:d30a3a12d387

134:d103a598548c Go to Latest

auth/client.go

Introduced scopes. Created a Scope type and a scopeStore interface, along with the memstore methods for the scopeStore. This will allow applications to define access with granularity, so users can grant access to some data, not _all_ data. We're operating on the assumption that there won't be an unreasonable number of scopes defined, so there is no paging operation included for the ListScopes method. This is a decision that may have to be revisited in the future, depending on usecases.

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