auth

Paddy 2015-12-14 Parent:8ecb60d29b0d

181:b7e685839a1b Go to Latest

auth/client.go

Break out scopes and events. This repo has gotten unwieldy, and there are portions of it that need to be imported by a large number of other packages. For example, scopes will be used in almost every API we write. Rather than importing the entirety of this codebase into every API we write, I've opted to move the scope logic out into a scopes package, with a subpackage for the defined types, which is all most projects actually want to import. We also define some event type constants, and importing those shouldn't require a project to import all our dependencies, either. So I made an events subpackage that just holds those constants. This package has become a little bit of a red-headed stepchild and is do for a refactor, but I'm trying to put that off as long as I can. The refactoring of our scopes stuff has left a bug wherein a token can be granted for scopes that don't exist. I'm going to need to revisit that, and also how to limit scopes to only be granted to the users that should be able to request them. But that's a battle for another day.

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