auth

Paddy 2015-03-21 Parent:93c758f57c69 Child:77db7c65216c

149:8267e1c8bcd1 Go to Latest

auth/client.go

Test our Postgres profileStore implementation. Update all our test cases to use time.Now().Round(time.Millisecond), because Go uses nanosecond precision on time values, but Postgres silently truncates that to millisecond precision. This caused our tests to report false failures that were just silent precision loss, not actual failures. Set up our authd server to use the Postgres store for profiles and automatically create a test scope when starting up. Log errors when creating Clients through the API, instead of just swallowing them and sending back cryptic act of god errors. Add a NewPostgres helper that returns a postgres profileStore from a connection string (passed through pq transparently). Add an Empty() bool helper to ProfileChange and BulkProfileChange types, so we can determine if there are any changes we need to act on easily. Log errors when creating Pofiles through the API, instead of just swalloing them and sending back cryptic act of god errors. Remove the ` quotes around field and table names, which are not supported in Postgres. This required adding a few functions/methods to pan. Detect situations where a profile was expected and not found, and return ErrProfileNotFound. Detect pq errors thrown when the profiles_pkey constraint is violated, and transform them to the ErrProfileAlreadyExists error. Detect empty ProfileChange and BulkProfileChange variables and abort the updateProfile and updateProfiles methods early, before invalid SQL is generated. Detect pq errors thrown when the logins_pkey constraint is violated, and transform them to the ErrLoginAlreadyExists error. Detect when removing a Login and no rows were affected, and return an ErrLoginNotFound. Create an sql dir with a postgres_init script that will initialize the schema of the tables expected in the database.

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