auth

Paddy 2015-06-29 Parent:8ecb60d29b0d Child:b7e685839a1b

175:aa14e29b666f Go to Latest

auth/client.go

Create Docker image for authd. Create a Dockerfile for authd, which will wrap the compiled Go binary up into a tiny little Docker image. Create an authd/build-docker.sh script that will build the statically-linked binary in a Docker container, so the authd Docker image can use it. We had to include ca-certificates.crt in the Dockerfile, as well, so we could communicate over SSL with things. A wrapper.sh file is included that will pull the JWT_SECRET environment variable out of a kubernetes secrets file, which is a handy wrapper to have. Finally, we added the authd/docker-authd binary to the .hgignore.

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