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