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