auth

Paddy 2015-04-19 Parent:77db7c65216c Child:cf1aef6eb81f

163:73e12d5a1124 Go to Latest

auth/client.go

Use postgres arrays for scope associations. Use the new pqarrays library I wrote to store Scope associations for Tokens and AuthorizationCodes, instead of using our hacky and abstraction-breaking many-to-many code. We also created the authStore.deleteAuthorizationCodesByProfileID method, to clear out the AuthorizationCodes that belong to a Profile (used when the Profile is deleted). So we added the implementation for memstore and for our postgres store. Call Context.DeleteAuthorizationCodesByProfileID when deleting a Profile to clean up after it. Rename sortedScopes to Scopes, which we use pqarrays.StringArray's methods on to fulfill the sql.Scanner and driver.Valuer interfaces. This lets us store Scopes in postgres arrays. Create a stringsToScopes helper function that creates Scope objects, with their IDs filled by the strings specified. Update our GrantType.Validate function signature to return Scopes instead of []string. Create a Scopes.Strings() helper method that returns a []string of the IDs of the Scopes. Update our SQL init file to use the new postgres array definition, instead of the many-to-many definition.

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