auth
auth/client.go
Attach our Scope type to AuthCodes and Tokens. When obtaining an AuthorizationCode or Token, attach a slice of strings, each one a Scope ID, instead of just attaching the encoded string the user passes in. This will allow us to change our Scope encoding down the line, and is more conceptually faithful. Also, if an authorization request is made with an invalid scope, return the invalid_scope error.
| 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@41 | 40 |
| paddy@57 | 41 // ErrEmptyChange is returned when a Change has all its properties set to nil. |
| paddy@57 | 42 ErrEmptyChange = errors.New("change must have at least one property set") |
| paddy@57 | 43 // ErrClientNameTooShort is returned when a Client's Name property is too short. |
| paddy@57 | 44 ErrClientNameTooShort = errors.New("client name must be at least 2 characters") |
| paddy@57 | 45 // ErrClientNameTooLong is returned when a Client's Name property is too long. |
| paddy@57 | 46 ErrClientNameTooLong = errors.New("client name must be at most 32 characters") |
| paddy@57 | 47 // ErrClientLogoTooLong is returned when a Client's Logo property is too long. |
| paddy@57 | 48 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters") |
| paddy@57 | 49 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL. |
| paddy@57 | 50 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL") |
| paddy@57 | 51 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long. |
| paddy@49 | 52 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters") |
| paddy@57 | 53 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL. |
| paddy@57 | 54 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL") |
| paddy@116 | 55 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL. |
| paddy@116 | 56 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL") |
| paddy@31 | 57 ) |
| paddy@31 | 58 |
| paddy@115 | 59 const ( |
| paddy@131 | 60 clientTypePublic = "public" |
| paddy@131 | 61 clientTypeConfidential = "confidential" |
| paddy@131 | 62 minClientNameLen = 2 |
| paddy@131 | 63 maxClientNameLen = 24 |
| paddy@131 | 64 defaultClientResponseSize = 20 |
| paddy@131 | 65 maxClientResponseSize = 50 |
| paddy@130 | 66 |
| paddy@130 | 67 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery |
| paddy@115 | 68 ) |
| paddy@115 | 69 |
| paddy@25 | 70 // Client represents a client that grants access |
| paddy@25 | 71 // to the auth server, exchanging grants for tokens, |
| paddy@25 | 72 // and tokens for access. |
| paddy@0 | 73 type Client struct { |
| paddy@116 | 74 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 75 Secret string `json:"secret,omitempty"` |
| paddy@116 | 76 OwnerID uuid.ID `json:"owner_id,omitempty"` |
| paddy@116 | 77 Name string `json:"name,omitempty"` |
| paddy@116 | 78 Logo string `json:"logo,omitempty"` |
| paddy@116 | 79 Website string `json:"website,omitempty"` |
| paddy@116 | 80 Type string `json:"type,omitempty"` |
| paddy@0 | 81 } |
| paddy@0 | 82 |
| paddy@57 | 83 // ApplyChange applies the properties of the passed |
| paddy@57 | 84 // ClientChange to the Client object it is called on. |
| paddy@39 | 85 func (c *Client) ApplyChange(change ClientChange) { |
| paddy@39 | 86 if change.Secret != nil { |
| paddy@39 | 87 c.Secret = *change.Secret |
| paddy@39 | 88 } |
| paddy@39 | 89 if change.OwnerID != nil { |
| paddy@39 | 90 c.OwnerID = change.OwnerID |
| paddy@39 | 91 } |
| paddy@39 | 92 if change.Name != nil { |
| paddy@39 | 93 c.Name = *change.Name |
| paddy@39 | 94 } |
| paddy@39 | 95 if change.Logo != nil { |
| paddy@39 | 96 c.Logo = *change.Logo |
| paddy@39 | 97 } |
| paddy@39 | 98 if change.Website != nil { |
| paddy@39 | 99 c.Website = *change.Website |
| paddy@39 | 100 } |
| paddy@39 | 101 } |
| paddy@39 | 102 |
| paddy@57 | 103 // ClientChange represents a bundle of options for |
| paddy@57 | 104 // updating a Client's mutable data. |
| paddy@31 | 105 type ClientChange struct { |
| paddy@41 | 106 Secret *string |
| paddy@41 | 107 OwnerID uuid.ID |
| paddy@41 | 108 Name *string |
| paddy@41 | 109 Logo *string |
| paddy@41 | 110 Website *string |
| paddy@31 | 111 } |
| paddy@31 | 112 |
| paddy@57 | 113 // Validate checks the ClientChange it is called on |
| paddy@57 | 114 // and asserts its internal validity, or lack thereof. |
| paddy@133 | 115 func (c ClientChange) Validate() []error { |
| paddy@133 | 116 errors := []error{} |
| paddy@42 | 117 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil { |
| paddy@133 | 118 errors = append(errors, ErrEmptyChange) |
| paddy@133 | 119 return errors |
| paddy@42 | 120 } |
| paddy@41 | 121 if c.Name != nil && len(*c.Name) < 2 { |
| paddy@133 | 122 errors = append(errors, ErrClientNameTooShort) |
| paddy@41 | 123 } |
| paddy@41 | 124 if c.Name != nil && len(*c.Name) > 32 { |
| paddy@133 | 125 errors = append(errors, ErrClientNameTooLong) |
| paddy@41 | 126 } |
| paddy@42 | 127 if c.Logo != nil && *c.Logo != "" { |
| paddy@42 | 128 if len(*c.Logo) > 1024 { |
| paddy@133 | 129 errors = append(errors, ErrClientLogoTooLong) |
| paddy@42 | 130 } |
| paddy@42 | 131 u, err := url.Parse(*c.Logo) |
| paddy@42 | 132 if err != nil || !u.IsAbs() { |
| paddy@133 | 133 errors = append(errors, ErrClientLogoNotURL) |
| paddy@42 | 134 } |
| paddy@41 | 135 } |
| paddy@42 | 136 if c.Website != nil && *c.Website != "" { |
| paddy@42 | 137 if len(*c.Website) > 140 { |
| paddy@133 | 138 errors = append(errors, ErrClientWebsiteTooLong) |
| paddy@42 | 139 } |
| paddy@42 | 140 u, err := url.Parse(*c.Website) |
| paddy@42 | 141 if err != nil || !u.IsAbs() { |
| paddy@133 | 142 errors = append(errors, ErrClientWebsiteNotURL) |
| paddy@42 | 143 } |
| paddy@41 | 144 } |
| paddy@133 | 145 return errors |
| paddy@39 | 146 } |
| paddy@39 | 147 |
| paddy@123 | 148 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) { |
| paddy@85 | 149 enc := json.NewEncoder(w) |
| paddy@85 | 150 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 151 if !fromAuthHeader { |
| paddy@85 | 152 clientIDStr = r.PostFormValue("client_id") |
| paddy@85 | 153 } |
| paddy@123 | 154 if clientIDStr == "" { |
| paddy@85 | 155 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 156 if fromAuthHeader { |
| paddy@85 | 157 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 158 } |
| paddy@85 | 159 renderJSONError(enc, "invalid_client") |
| paddy@123 | 160 return nil, "", false |
| paddy@123 | 161 } |
| paddy@129 | 162 if !allowPublic && !fromAuthHeader { |
| paddy@129 | 163 w.WriteHeader(http.StatusBadRequest) |
| paddy@129 | 164 renderJSONError(enc, "unauthorized_client") |
| paddy@129 | 165 return nil, "", false |
| paddy@129 | 166 } |
| paddy@123 | 167 clientID, err := uuid.Parse(clientIDStr) |
| paddy@123 | 168 if err != nil { |
| paddy@123 | 169 log.Println("Error decoding client ID:", err) |
| paddy@123 | 170 w.WriteHeader(http.StatusUnauthorized) |
| paddy@123 | 171 if fromAuthHeader { |
| paddy@123 | 172 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@123 | 173 } |
| paddy@123 | 174 renderJSONError(enc, "invalid_client") |
| paddy@123 | 175 return nil, "", false |
| paddy@123 | 176 } |
| paddy@123 | 177 return clientID, clientSecret, true |
| paddy@123 | 178 } |
| paddy@123 | 179 |
| paddy@123 | 180 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) { |
| paddy@123 | 181 enc := json.NewEncoder(w) |
| paddy@123 | 182 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic) |
| paddy@123 | 183 if !ok { |
| paddy@85 | 184 return nil, false |
| paddy@85 | 185 } |
| paddy@123 | 186 _, _, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 187 client, err := context.GetClient(clientID) |
| paddy@85 | 188 if err == ErrClientNotFound { |
| paddy@85 | 189 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 190 if fromAuthHeader { |
| paddy@85 | 191 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 192 } |
| paddy@85 | 193 renderJSONError(enc, "invalid_client") |
| paddy@85 | 194 return nil, false |
| paddy@85 | 195 } else if err != nil { |
| paddy@85 | 196 w.WriteHeader(http.StatusInternalServerError) |
| paddy@85 | 197 renderJSONError(enc, "server_error") |
| paddy@85 | 198 return nil, false |
| paddy@85 | 199 } |
| paddy@113 | 200 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret. |
| paddy@85 | 201 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 202 if fromAuthHeader { |
| paddy@85 | 203 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 204 } |
| paddy@85 | 205 renderJSONError(enc, "invalid_client") |
| paddy@85 | 206 return nil, false |
| paddy@85 | 207 } |
| paddy@85 | 208 return clientID, true |
| paddy@85 | 209 } |
| paddy@85 | 210 |
| paddy@57 | 211 // Endpoint represents a single URI that a Client |
| paddy@57 | 212 // controls. Users will be redirected to these URIs |
| paddy@57 | 213 // following successful authorization grants and |
| paddy@57 | 214 // exchanges for access tokens. |
| paddy@41 | 215 type Endpoint struct { |
| paddy@116 | 216 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 217 ClientID uuid.ID `json:"client_id,omitempty"` |
| paddy@116 | 218 URI string `json:"uri,omitempty"` |
| paddy@116 | 219 NormalizedURI string `json:"-"` |
| paddy@116 | 220 Added time.Time `json:"added,omitempty"` |
| paddy@116 | 221 } |
| paddy@116 | 222 |
| paddy@116 | 223 func normalizeURIString(in string) (string, error) { |
| paddy@130 | 224 n, err := purell.NormalizeURLString(in, normalizeFlags) |
| paddy@116 | 225 if err != nil { |
| paddy@116 | 226 log.Println(err) |
| paddy@116 | 227 return in, ErrEndpointURINotURL |
| paddy@116 | 228 } |
| paddy@116 | 229 return n, nil |
| paddy@116 | 230 } |
| paddy@116 | 231 |
| paddy@116 | 232 func normalizeURI(in *url.URL) string { |
| paddy@130 | 233 return purell.NormalizeURL(in, normalizeFlags) |
| paddy@41 | 234 } |
| paddy@41 | 235 |
| paddy@41 | 236 type sortedEndpoints []Endpoint |
| paddy@41 | 237 |
| paddy@41 | 238 func (s sortedEndpoints) Len() int { |
| paddy@41 | 239 return len(s) |
| paddy@41 | 240 } |
| paddy@41 | 241 |
| paddy@41 | 242 func (s sortedEndpoints) Less(i, j int) bool { |
| paddy@41 | 243 return s[i].Added.Before(s[j].Added) |
| paddy@41 | 244 } |
| paddy@41 | 245 |
| paddy@41 | 246 func (s sortedEndpoints) Swap(i, j int) { |
| paddy@41 | 247 s[i], s[j] = s[j], s[i] |
| paddy@41 | 248 } |
| paddy@41 | 249 |
| paddy@57 | 250 type clientStore interface { |
| paddy@57 | 251 getClient(id uuid.ID) (Client, error) |
| paddy@57 | 252 saveClient(client Client) error |
| paddy@57 | 253 updateClient(id uuid.ID, change ClientChange) error |
| paddy@57 | 254 deleteClient(id uuid.ID) error |
| paddy@57 | 255 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) |
| paddy@41 | 256 |
| paddy@115 | 257 addEndpoints(client uuid.ID, endpoint []Endpoint) error |
| paddy@57 | 258 removeEndpoint(client, endpoint uuid.ID) error |
| paddy@58 | 259 checkEndpoint(client uuid.ID, endpoint string) (bool, error) |
| paddy@57 | 260 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) |
| paddy@57 | 261 countEndpoints(client uuid.ID) (int64, error) |
| paddy@0 | 262 } |
| paddy@31 | 263 |
| paddy@57 | 264 func (m *memstore) getClient(id uuid.ID) (Client, error) { |
| paddy@31 | 265 m.clientLock.RLock() |
| paddy@31 | 266 defer m.clientLock.RUnlock() |
| paddy@31 | 267 c, ok := m.clients[id.String()] |
| paddy@31 | 268 if !ok { |
| paddy@31 | 269 return Client{}, ErrClientNotFound |
| paddy@31 | 270 } |
| paddy@31 | 271 return c, nil |
| paddy@31 | 272 } |
| paddy@31 | 273 |
| paddy@57 | 274 func (m *memstore) saveClient(client Client) error { |
| paddy@31 | 275 m.clientLock.Lock() |
| paddy@31 | 276 defer m.clientLock.Unlock() |
| paddy@31 | 277 if _, ok := m.clients[client.ID.String()]; ok { |
| paddy@31 | 278 return ErrClientAlreadyExists |
| paddy@31 | 279 } |
| paddy@31 | 280 m.clients[client.ID.String()] = client |
| paddy@31 | 281 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID) |
| paddy@31 | 282 return nil |
| paddy@31 | 283 } |
| paddy@31 | 284 |
| paddy@57 | 285 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error { |
| paddy@39 | 286 m.clientLock.Lock() |
| paddy@39 | 287 defer m.clientLock.Unlock() |
| paddy@39 | 288 c, ok := m.clients[id.String()] |
| paddy@39 | 289 if !ok { |
| paddy@39 | 290 return ErrClientNotFound |
| paddy@39 | 291 } |
| paddy@39 | 292 c.ApplyChange(change) |
| paddy@39 | 293 m.clients[id.String()] = c |
| paddy@31 | 294 return nil |
| paddy@31 | 295 } |
| paddy@31 | 296 |
| paddy@57 | 297 func (m *memstore) deleteClient(id uuid.ID) error { |
| paddy@57 | 298 client, err := m.getClient(id) |
| paddy@31 | 299 if err != nil { |
| paddy@31 | 300 return err |
| paddy@31 | 301 } |
| paddy@31 | 302 m.clientLock.Lock() |
| paddy@31 | 303 defer m.clientLock.Unlock() |
| paddy@31 | 304 delete(m.clients, id.String()) |
| paddy@31 | 305 pos := -1 |
| paddy@31 | 306 for p, item := range m.profileClientLookup[client.OwnerID.String()] { |
| paddy@31 | 307 if item.Equal(id) { |
| paddy@31 | 308 pos = p |
| paddy@31 | 309 break |
| paddy@31 | 310 } |
| paddy@31 | 311 } |
| paddy@31 | 312 if pos >= 0 { |
| paddy@31 | 313 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...) |
| paddy@31 | 314 } |
| paddy@31 | 315 return nil |
| paddy@31 | 316 } |
| paddy@31 | 317 |
| paddy@57 | 318 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) { |
| paddy@33 | 319 ids := m.lookupClientsByProfileID(ownerID.String()) |
| paddy@31 | 320 if len(ids) > num+offset { |
| paddy@31 | 321 ids = ids[offset : num+offset] |
| paddy@31 | 322 } else if len(ids) > offset { |
| paddy@31 | 323 ids = ids[offset:] |
| paddy@31 | 324 } else { |
| paddy@31 | 325 return []Client{}, nil |
| paddy@31 | 326 } |
| paddy@31 | 327 clients := []Client{} |
| paddy@31 | 328 for _, id := range ids { |
| paddy@57 | 329 client, err := m.getClient(id) |
| paddy@31 | 330 if err != nil { |
| paddy@31 | 331 return []Client{}, err |
| paddy@31 | 332 } |
| paddy@31 | 333 clients = append(clients, client) |
| paddy@31 | 334 } |
| paddy@31 | 335 return clients, nil |
| paddy@31 | 336 } |
| paddy@41 | 337 |
| paddy@115 | 338 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error { |
| paddy@41 | 339 m.endpointLock.Lock() |
| paddy@41 | 340 defer m.endpointLock.Unlock() |
| paddy@115 | 341 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...) |
| paddy@41 | 342 return nil |
| paddy@41 | 343 } |
| paddy@41 | 344 |
| paddy@57 | 345 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error { |
| paddy@41 | 346 m.endpointLock.Lock() |
| paddy@41 | 347 defer m.endpointLock.Unlock() |
| paddy@41 | 348 pos := -1 |
| paddy@41 | 349 for p, item := range m.endpoints[client.String()] { |
| paddy@41 | 350 if item.ID.Equal(endpoint) { |
| paddy@41 | 351 pos = p |
| paddy@41 | 352 break |
| paddy@41 | 353 } |
| paddy@41 | 354 } |
| paddy@41 | 355 if pos >= 0 { |
| paddy@41 | 356 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...) |
| paddy@41 | 357 } |
| paddy@41 | 358 return nil |
| paddy@41 | 359 } |
| paddy@41 | 360 |
| paddy@58 | 361 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) { |
| paddy@41 | 362 m.endpointLock.RLock() |
| paddy@41 | 363 defer m.endpointLock.RUnlock() |
| paddy@41 | 364 for _, candidate := range m.endpoints[client.String()] { |
| paddy@116 | 365 if endpoint == candidate.NormalizedURI { |
| paddy@41 | 366 return true, nil |
| paddy@41 | 367 } |
| paddy@41 | 368 } |
| paddy@41 | 369 return false, nil |
| paddy@41 | 370 } |
| paddy@41 | 371 |
| paddy@57 | 372 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) { |
| paddy@41 | 373 m.endpointLock.RLock() |
| paddy@41 | 374 defer m.endpointLock.RUnlock() |
| paddy@41 | 375 return m.endpoints[client.String()], nil |
| paddy@41 | 376 } |
| paddy@54 | 377 |
| paddy@57 | 378 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) { |
| paddy@54 | 379 m.endpointLock.RLock() |
| paddy@54 | 380 defer m.endpointLock.RUnlock() |
| paddy@54 | 381 return int64(len(m.endpoints[client.String()])), nil |
| paddy@54 | 382 } |
| paddy@108 | 383 |
| paddy@108 | 384 type newClientReq struct { |
| paddy@108 | 385 Name string `json:"name"` |
| paddy@108 | 386 Logo string `json:"logo"` |
| paddy@108 | 387 Website string `json:"website"` |
| paddy@108 | 388 Type string `json:"type"` |
| paddy@108 | 389 Endpoints []string `json:"endpoints"` |
| paddy@108 | 390 } |
| paddy@108 | 391 |
| paddy@108 | 392 func RegisterClientHandlers(r *mux.Router, context Context) { |
| paddy@108 | 393 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST") |
| paddy@131 | 394 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET") |
| paddy@131 | 395 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET") |
| paddy@133 | 396 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH") |
| paddy@128 | 397 // BUG(paddy): We need to implement a handler to delete a client. Also, what should that do with the grants and tokens belonging to that client? |
| paddy@128 | 398 // BUG(paddy): We need to implement a handler to add an endpoint to a client. |
| paddy@128 | 399 // BUG(paddy): We need to implement a handler to remove an endpoint from a client. |
| paddy@128 | 400 // BUG(paddy): We need to implement a handler to list endpoints. |
| paddy@108 | 401 } |
| paddy@108 | 402 |
| paddy@108 | 403 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@115 | 404 errors := []requestError{} |
| paddy@108 | 405 username, password, ok := r.BasicAuth() |
| paddy@108 | 406 if !ok { |
| paddy@115 | 407 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@115 | 408 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@108 | 409 return |
| paddy@108 | 410 } |
| paddy@108 | 411 profile, err := authenticate(username, password, c) |
| paddy@108 | 412 if err != nil { |
| paddy@115 | 413 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@115 | 414 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@108 | 415 return |
| paddy@108 | 416 } |
| paddy@108 | 417 var req newClientReq |
| paddy@108 | 418 decoder := json.NewDecoder(r.Body) |
| paddy@108 | 419 err = decoder.Decode(&req) |
| paddy@108 | 420 if err != nil { |
| paddy@108 | 421 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@108 | 422 return |
| paddy@108 | 423 } |
| paddy@116 | 424 if req.Type == "" { |
| paddy@116 | 425 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"}) |
| paddy@116 | 426 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential { |
| paddy@115 | 427 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"}) |
| paddy@116 | 428 } |
| paddy@116 | 429 if req.Name == "" { |
| paddy@116 | 430 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"}) |
| paddy@116 | 431 } else if len(req.Name) < minClientNameLen { |
| paddy@116 | 432 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) |
| paddy@116 | 433 } else if len(req.Name) > maxClientNameLen { |
| paddy@116 | 434 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) |
| paddy@116 | 435 } |
| paddy@116 | 436 if len(errors) > 0 { |
| paddy@115 | 437 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@108 | 438 return |
| paddy@108 | 439 } |
| paddy@108 | 440 client := Client{ |
| paddy@108 | 441 ID: uuid.NewID(), |
| paddy@108 | 442 OwnerID: profile.ID, |
| paddy@108 | 443 Name: req.Name, |
| paddy@108 | 444 Logo: req.Logo, |
| paddy@108 | 445 Website: req.Website, |
| paddy@108 | 446 Type: req.Type, |
| paddy@108 | 447 } |
| paddy@118 | 448 if client.Type == clientTypeConfidential { |
| paddy@115 | 449 secret := make([]byte, 32) |
| paddy@115 | 450 _, err = rand.Read(secret) |
| paddy@115 | 451 if err != nil { |
| paddy@115 | 452 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@115 | 453 return |
| paddy@115 | 454 } |
| paddy@115 | 455 client.Secret = hex.EncodeToString(secret) |
| paddy@115 | 456 } |
| paddy@108 | 457 err = c.SaveClient(client) |
| paddy@108 | 458 if err != nil { |
| paddy@115 | 459 if err == ErrClientAlreadyExists { |
| paddy@115 | 460 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"}) |
| paddy@115 | 461 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@115 | 462 return |
| paddy@115 | 463 } |
| paddy@115 | 464 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@108 | 465 return |
| paddy@108 | 466 } |
| paddy@108 | 467 endpoints := []Endpoint{} |
| paddy@115 | 468 for pos, u := range req.Endpoints { |
| paddy@108 | 469 uri, err := url.Parse(u) |
| paddy@108 | 470 if err != nil { |
| paddy@115 | 471 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@108 | 472 continue |
| paddy@108 | 473 } |
| paddy@116 | 474 if !uri.IsAbs() { |
| paddy@116 | 475 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@116 | 476 continue |
| paddy@116 | 477 } |
| paddy@108 | 478 endpoint := Endpoint{ |
| paddy@108 | 479 ID: uuid.NewID(), |
| paddy@108 | 480 ClientID: client.ID, |
| paddy@116 | 481 URI: uri.String(), |
| paddy@108 | 482 Added: time.Now(), |
| paddy@108 | 483 } |
| paddy@108 | 484 endpoints = append(endpoints, endpoint) |
| paddy@108 | 485 } |
| paddy@115 | 486 err = c.AddEndpoints(client.ID, endpoints) |
| paddy@115 | 487 if err != nil { |
| paddy@115 | 488 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@115 | 489 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}}) |
| paddy@115 | 490 return |
| paddy@115 | 491 } |
| paddy@108 | 492 resp := response{ |
| paddy@108 | 493 Clients: []Client{client}, |
| paddy@108 | 494 Endpoints: endpoints, |
| paddy@116 | 495 Errors: errors, |
| paddy@108 | 496 } |
| paddy@108 | 497 encode(w, r, http.StatusCreated, resp) |
| paddy@108 | 498 } |
| paddy@121 | 499 |
| paddy@131 | 500 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@131 | 501 errors := []requestError{} |
| paddy@131 | 502 vars := mux.Vars(r) |
| paddy@131 | 503 if vars["id"] == "" { |
| paddy@131 | 504 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@131 | 505 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 506 return |
| paddy@131 | 507 } |
| paddy@131 | 508 id, err := uuid.Parse(vars["id"]) |
| paddy@131 | 509 if err != nil { |
| paddy@131 | 510 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@131 | 511 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 512 return |
| paddy@131 | 513 } |
| paddy@131 | 514 client, err := c.GetClient(id) |
| paddy@131 | 515 if err != nil { |
| paddy@131 | 516 if err == ErrClientNotFound { |
| paddy@131 | 517 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@131 | 518 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 519 return |
| paddy@131 | 520 } |
| paddy@131 | 521 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@131 | 522 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 523 return |
| paddy@131 | 524 } |
| paddy@131 | 525 client.Secret = "" |
| paddy@131 | 526 // BUG(paddy): How should auth be handled for retrieving clients? |
| paddy@131 | 527 resp := response{ |
| paddy@131 | 528 Clients: []Client{client}, |
| paddy@131 | 529 Errors: errors, |
| paddy@131 | 530 } |
| paddy@131 | 531 encode(w, r, http.StatusOK, resp) |
| paddy@131 | 532 } |
| paddy@131 | 533 |
| paddy@131 | 534 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@131 | 535 errors := []requestError{} |
| paddy@131 | 536 var err error |
| paddy@131 | 537 // BUG(paddy): If ids are provided in query params, retrieve only those clients |
| paddy@131 | 538 // BUG(paddy): We should have auth when listing clients |
| paddy@131 | 539 num := defaultClientResponseSize |
| paddy@131 | 540 offset := 0 |
| paddy@131 | 541 ownerIDStr := r.URL.Query().Get("owner_id") |
| paddy@131 | 542 numStr := r.URL.Query().Get("num") |
| paddy@131 | 543 offsetStr := r.URL.Query().Get("offset") |
| paddy@131 | 544 if numStr != "" { |
| paddy@131 | 545 num, err = strconv.Atoi(numStr) |
| paddy@131 | 546 if err != nil { |
| paddy@131 | 547 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"}) |
| paddy@131 | 548 } |
| paddy@131 | 549 if num > maxClientResponseSize { |
| paddy@131 | 550 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"}) |
| paddy@131 | 551 } |
| paddy@131 | 552 } |
| paddy@131 | 553 if offsetStr != "" { |
| paddy@131 | 554 offset, err = strconv.Atoi(offsetStr) |
| paddy@131 | 555 if err != nil { |
| paddy@131 | 556 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"}) |
| paddy@131 | 557 } |
| paddy@131 | 558 } |
| paddy@131 | 559 if ownerIDStr == "" { |
| paddy@131 | 560 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"}) |
| paddy@131 | 561 } |
| paddy@131 | 562 if len(errors) > 0 { |
| paddy@131 | 563 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 564 return |
| paddy@131 | 565 } |
| paddy@131 | 566 ownerID, err := uuid.Parse(ownerIDStr) |
| paddy@131 | 567 if err != nil { |
| paddy@131 | 568 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"}) |
| paddy@131 | 569 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 570 return |
| paddy@131 | 571 } |
| paddy@131 | 572 clients, err := c.ListClientsByOwner(ownerID, num, offset) |
| paddy@131 | 573 if err != nil { |
| paddy@131 | 574 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@131 | 575 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 576 return |
| paddy@131 | 577 } |
| paddy@131 | 578 for pos, client := range clients { |
| paddy@131 | 579 client.Secret = "" |
| paddy@131 | 580 clients[pos] = client |
| paddy@131 | 581 } |
| paddy@131 | 582 resp := response{ |
| paddy@131 | 583 Clients: clients, |
| paddy@131 | 584 Errors: errors, |
| paddy@131 | 585 } |
| paddy@131 | 586 encode(w, r, http.StatusOK, resp) |
| paddy@131 | 587 } |
| paddy@131 | 588 |
| paddy@133 | 589 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@133 | 590 errors := []requestError{} |
| paddy@133 | 591 vars := mux.Vars(r) |
| paddy@133 | 592 if _, ok := vars["id"]; !ok { |
| paddy@133 | 593 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@133 | 594 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 595 return |
| paddy@133 | 596 } |
| paddy@133 | 597 var change ClientChange |
| paddy@133 | 598 err := decode(r, &change) |
| paddy@133 | 599 if err != nil { |
| paddy@133 | 600 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"}) |
| paddy@133 | 601 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 602 return |
| paddy@133 | 603 } |
| paddy@133 | 604 errs := change.Validate() |
| paddy@133 | 605 for _, err := range errs { |
| paddy@133 | 606 switch err { |
| paddy@133 | 607 case ErrEmptyChange: |
| paddy@133 | 608 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"}) |
| paddy@133 | 609 case ErrClientNameTooShort: |
| paddy@133 | 610 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) |
| paddy@133 | 611 case ErrClientNameTooLong: |
| paddy@133 | 612 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) |
| paddy@133 | 613 case ErrClientLogoTooLong: |
| paddy@133 | 614 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"}) |
| paddy@133 | 615 case ErrClientLogoNotURL: |
| paddy@133 | 616 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"}) |
| paddy@133 | 617 case ErrClientWebsiteTooLong: |
| paddy@133 | 618 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"}) |
| paddy@133 | 619 case ErrClientWebsiteNotURL: |
| paddy@133 | 620 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"}) |
| paddy@133 | 621 default: |
| paddy@133 | 622 log.Println("Unrecognised error from client change validation:", err) |
| paddy@133 | 623 } |
| paddy@133 | 624 } |
| paddy@133 | 625 id, err := uuid.Parse(vars["id"]) |
| paddy@133 | 626 if err != nil { |
| paddy@133 | 627 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@133 | 628 } |
| paddy@133 | 629 if len(errors) > 0 { |
| paddy@133 | 630 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 631 return |
| paddy@133 | 632 } |
| paddy@133 | 633 client, err := c.GetClient(id) |
| paddy@133 | 634 if err == ErrClientNotFound { |
| paddy@133 | 635 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@133 | 636 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@133 | 637 return |
| paddy@133 | 638 } else if err != nil { |
| paddy@133 | 639 log.Println("Error retrieving client:", err) |
| paddy@133 | 640 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@133 | 641 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@133 | 642 return |
| paddy@133 | 643 } |
| paddy@133 | 644 if change.Secret != nil && client.Type == clientTypeConfidential { |
| paddy@133 | 645 secret := make([]byte, 32) |
| paddy@133 | 646 _, err = rand.Read(secret) |
| paddy@133 | 647 if err != nil { |
| paddy@133 | 648 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@133 | 649 return |
| paddy@133 | 650 } |
| paddy@133 | 651 newSecret := hex.EncodeToString(secret) |
| paddy@133 | 652 change.Secret = &newSecret |
| paddy@133 | 653 } |
| paddy@133 | 654 err = c.UpdateClient(id, change) |
| paddy@133 | 655 if err != nil { |
| paddy@133 | 656 log.Println("Error updating client:", err) |
| paddy@133 | 657 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@133 | 658 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@133 | 659 return |
| paddy@133 | 660 } |
| paddy@133 | 661 client.ApplyChange(change) |
| paddy@133 | 662 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors}) |
| paddy@133 | 663 return |
| paddy@133 | 664 } |
| paddy@133 | 665 |
| paddy@135 | 666 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) { |
| paddy@135 | 667 scopes = strings.Split(r.PostFormValue("scope"), " ") |
| paddy@121 | 668 valid = true |
| paddy@121 | 669 return |
| paddy@121 | 670 } |
| paddy@124 | 671 |
| paddy@124 | 672 func clientCredentialsAuditString(r *http.Request) string { |
| paddy@124 | 673 return "client_credentials" |
| paddy@124 | 674 } |