auth
auth/client.go
Add our BUG notices. Rather than keeping the list of things to implement or test on sticky notes attached to my monitor, let's give them BUG designations within the code. Now `godoc . bugs` will list them out for us. Isn't that nice?
| 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@41 | 12 "time" |
| paddy@31 | 13 |
| paddy@116 | 14 "github.com/PuerkitoBio/purell" |
| paddy@116 | 15 "github.com/gorilla/mux" |
| paddy@116 | 16 |
| paddy@107 | 17 "code.secondbit.org/uuid.hg" |
| paddy@0 | 18 ) |
| paddy@0 | 19 |
| paddy@121 | 20 func init() { |
| paddy@121 | 21 RegisterGrantType("client_credentials", GrantType{ |
| paddy@121 | 22 Validate: clientCredentialsValidate, |
| paddy@121 | 23 Invalidate: nil, |
| paddy@121 | 24 IssuesRefresh: true, |
| paddy@121 | 25 ReturnToken: RenderJSONToken, |
| paddy@123 | 26 AllowsPublic: false, |
| paddy@124 | 27 AuditString: clientCredentialsAuditString, |
| paddy@121 | 28 }) |
| paddy@121 | 29 } |
| paddy@121 | 30 |
| paddy@31 | 31 var ( |
| paddy@57 | 32 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first. |
| paddy@57 | 33 ErrNoClientStore = errors.New("no clientStore was specified for the Context") |
| paddy@57 | 34 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore. |
| paddy@57 | 35 ErrClientNotFound = errors.New("client not found in clientStore") |
| paddy@57 | 36 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with |
| paddy@57 | 37 // the same ID already exists in the clientStore. |
| paddy@57 | 38 ErrClientAlreadyExists = errors.New("client already exists in clientStore") |
| paddy@41 | 39 |
| paddy@57 | 40 // ErrEmptyChange is returned when a Change has all its properties set to nil. |
| paddy@57 | 41 ErrEmptyChange = errors.New("change must have at least one property set") |
| paddy@57 | 42 // ErrClientNameTooShort is returned when a Client's Name property is too short. |
| paddy@57 | 43 ErrClientNameTooShort = errors.New("client name must be at least 2 characters") |
| paddy@57 | 44 // ErrClientNameTooLong is returned when a Client's Name property is too long. |
| paddy@57 | 45 ErrClientNameTooLong = errors.New("client name must be at most 32 characters") |
| paddy@57 | 46 // ErrClientLogoTooLong is returned when a Client's Logo property is too long. |
| paddy@57 | 47 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters") |
| paddy@57 | 48 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL. |
| paddy@57 | 49 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL") |
| paddy@57 | 50 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long. |
| paddy@49 | 51 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters") |
| paddy@57 | 52 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL. |
| paddy@57 | 53 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL") |
| paddy@116 | 54 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL. |
| paddy@116 | 55 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL") |
| paddy@31 | 56 ) |
| paddy@31 | 57 |
| paddy@115 | 58 const ( |
| paddy@115 | 59 clientTypePublic = "public" |
| paddy@115 | 60 clientTypeConfidential = "confidential" |
| paddy@116 | 61 minClientNameLen = 2 |
| paddy@116 | 62 maxClientNameLen = 24 |
| paddy@115 | 63 ) |
| paddy@115 | 64 |
| paddy@25 | 65 // Client represents a client that grants access |
| paddy@25 | 66 // to the auth server, exchanging grants for tokens, |
| paddy@25 | 67 // and tokens for access. |
| paddy@0 | 68 type Client struct { |
| paddy@116 | 69 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 70 Secret string `json:"secret,omitempty"` |
| paddy@116 | 71 OwnerID uuid.ID `json:"owner_id,omitempty"` |
| paddy@116 | 72 Name string `json:"name,omitempty"` |
| paddy@116 | 73 Logo string `json:"logo,omitempty"` |
| paddy@116 | 74 Website string `json:"website,omitempty"` |
| paddy@116 | 75 Type string `json:"type,omitempty"` |
| paddy@0 | 76 } |
| paddy@0 | 77 |
| paddy@57 | 78 // ApplyChange applies the properties of the passed |
| paddy@57 | 79 // ClientChange to the Client object it is called on. |
| paddy@39 | 80 func (c *Client) ApplyChange(change ClientChange) { |
| paddy@39 | 81 if change.Secret != nil { |
| paddy@39 | 82 c.Secret = *change.Secret |
| paddy@39 | 83 } |
| paddy@39 | 84 if change.OwnerID != nil { |
| paddy@39 | 85 c.OwnerID = change.OwnerID |
| paddy@39 | 86 } |
| paddy@39 | 87 if change.Name != nil { |
| paddy@39 | 88 c.Name = *change.Name |
| paddy@39 | 89 } |
| paddy@39 | 90 if change.Logo != nil { |
| paddy@39 | 91 c.Logo = *change.Logo |
| paddy@39 | 92 } |
| paddy@39 | 93 if change.Website != nil { |
| paddy@39 | 94 c.Website = *change.Website |
| paddy@39 | 95 } |
| paddy@39 | 96 } |
| paddy@39 | 97 |
| paddy@57 | 98 // ClientChange represents a bundle of options for |
| paddy@57 | 99 // updating a Client's mutable data. |
| paddy@31 | 100 type ClientChange struct { |
| paddy@41 | 101 Secret *string |
| paddy@41 | 102 OwnerID uuid.ID |
| paddy@41 | 103 Name *string |
| paddy@41 | 104 Logo *string |
| paddy@41 | 105 Website *string |
| paddy@31 | 106 } |
| paddy@31 | 107 |
| paddy@57 | 108 // Validate checks the ClientChange it is called on |
| paddy@57 | 109 // and asserts its internal validity, or lack thereof. |
| paddy@39 | 110 func (c ClientChange) Validate() error { |
| paddy@42 | 111 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil { |
| paddy@42 | 112 return ErrEmptyChange |
| paddy@42 | 113 } |
| paddy@41 | 114 if c.Name != nil && len(*c.Name) < 2 { |
| paddy@41 | 115 return ErrClientNameTooShort |
| paddy@41 | 116 } |
| paddy@41 | 117 if c.Name != nil && len(*c.Name) > 32 { |
| paddy@41 | 118 return ErrClientNameTooLong |
| paddy@41 | 119 } |
| paddy@42 | 120 if c.Logo != nil && *c.Logo != "" { |
| paddy@42 | 121 if len(*c.Logo) > 1024 { |
| paddy@42 | 122 return ErrClientLogoTooLong |
| paddy@42 | 123 } |
| paddy@42 | 124 u, err := url.Parse(*c.Logo) |
| paddy@42 | 125 if err != nil || !u.IsAbs() { |
| paddy@42 | 126 return ErrClientLogoNotURL |
| paddy@42 | 127 } |
| paddy@41 | 128 } |
| paddy@42 | 129 if c.Website != nil && *c.Website != "" { |
| paddy@42 | 130 if len(*c.Website) > 140 { |
| paddy@42 | 131 return ErrClientWebsiteTooLong |
| paddy@42 | 132 } |
| paddy@42 | 133 u, err := url.Parse(*c.Website) |
| paddy@42 | 134 if err != nil || !u.IsAbs() { |
| paddy@42 | 135 return ErrClientWebsiteNotURL |
| paddy@42 | 136 } |
| paddy@41 | 137 } |
| paddy@39 | 138 return nil |
| paddy@39 | 139 } |
| paddy@39 | 140 |
| paddy@123 | 141 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) { |
| paddy@85 | 142 enc := json.NewEncoder(w) |
| paddy@85 | 143 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 144 if !fromAuthHeader { |
| paddy@85 | 145 clientIDStr = r.PostFormValue("client_id") |
| paddy@85 | 146 } |
| paddy@123 | 147 if clientIDStr == "" { |
| paddy@85 | 148 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 149 if fromAuthHeader { |
| paddy@85 | 150 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 151 } |
| paddy@85 | 152 renderJSONError(enc, "invalid_client") |
| paddy@123 | 153 return nil, "", false |
| paddy@123 | 154 } |
| paddy@123 | 155 clientID, err := uuid.Parse(clientIDStr) |
| paddy@123 | 156 if err != nil { |
| paddy@123 | 157 log.Println("Error decoding client ID:", err) |
| paddy@123 | 158 w.WriteHeader(http.StatusUnauthorized) |
| paddy@123 | 159 if fromAuthHeader { |
| paddy@123 | 160 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@123 | 161 } |
| paddy@123 | 162 renderJSONError(enc, "invalid_client") |
| paddy@123 | 163 return nil, "", false |
| paddy@123 | 164 } |
| paddy@123 | 165 if !allowPublic && !fromAuthHeader { |
| paddy@123 | 166 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 167 renderJSONError(enc, "unauthorized_client") |
| paddy@123 | 168 return nil, "", false |
| paddy@123 | 169 } |
| paddy@123 | 170 return clientID, clientSecret, true |
| paddy@123 | 171 } |
| paddy@123 | 172 |
| paddy@123 | 173 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) { |
| paddy@123 | 174 enc := json.NewEncoder(w) |
| paddy@123 | 175 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic) |
| paddy@123 | 176 if !ok { |
| paddy@85 | 177 return nil, false |
| paddy@85 | 178 } |
| paddy@123 | 179 _, _, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 180 client, err := context.GetClient(clientID) |
| paddy@85 | 181 if err == ErrClientNotFound { |
| paddy@85 | 182 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 183 if fromAuthHeader { |
| paddy@85 | 184 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 185 } |
| paddy@85 | 186 renderJSONError(enc, "invalid_client") |
| paddy@85 | 187 return nil, false |
| paddy@85 | 188 } else if err != nil { |
| paddy@85 | 189 w.WriteHeader(http.StatusInternalServerError) |
| paddy@85 | 190 renderJSONError(enc, "server_error") |
| paddy@85 | 191 return nil, false |
| paddy@85 | 192 } |
| paddy@113 | 193 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret. |
| paddy@85 | 194 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 195 if fromAuthHeader { |
| paddy@85 | 196 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 197 } |
| paddy@85 | 198 renderJSONError(enc, "invalid_client") |
| paddy@85 | 199 return nil, false |
| paddy@85 | 200 } |
| paddy@85 | 201 return clientID, true |
| paddy@85 | 202 } |
| paddy@85 | 203 |
| paddy@57 | 204 // Endpoint represents a single URI that a Client |
| paddy@57 | 205 // controls. Users will be redirected to these URIs |
| paddy@57 | 206 // following successful authorization grants and |
| paddy@57 | 207 // exchanges for access tokens. |
| paddy@41 | 208 type Endpoint struct { |
| paddy@116 | 209 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 210 ClientID uuid.ID `json:"client_id,omitempty"` |
| paddy@116 | 211 URI string `json:"uri,omitempty"` |
| paddy@116 | 212 NormalizedURI string `json:"-"` |
| paddy@116 | 213 Added time.Time `json:"added,omitempty"` |
| paddy@116 | 214 } |
| paddy@116 | 215 |
| paddy@116 | 216 func normalizeURIString(in string) (string, error) { |
| paddy@116 | 217 n, err := purell.NormalizeURLString(in, purell.FlagsUsuallySafeNonGreedy|purell.FlagSortQuery) |
| paddy@116 | 218 if err != nil { |
| paddy@116 | 219 log.Println(err) |
| paddy@116 | 220 return in, ErrEndpointURINotURL |
| paddy@116 | 221 } |
| paddy@116 | 222 return n, nil |
| paddy@116 | 223 } |
| paddy@116 | 224 |
| paddy@116 | 225 func normalizeURI(in *url.URL) string { |
| paddy@116 | 226 return purell.NormalizeURL(in, purell.FlagsUsuallySafeNonGreedy|purell.FlagSortQuery) |
| paddy@41 | 227 } |
| paddy@41 | 228 |
| paddy@41 | 229 type sortedEndpoints []Endpoint |
| paddy@41 | 230 |
| paddy@41 | 231 func (s sortedEndpoints) Len() int { |
| paddy@41 | 232 return len(s) |
| paddy@41 | 233 } |
| paddy@41 | 234 |
| paddy@41 | 235 func (s sortedEndpoints) Less(i, j int) bool { |
| paddy@41 | 236 return s[i].Added.Before(s[j].Added) |
| paddy@41 | 237 } |
| paddy@41 | 238 |
| paddy@41 | 239 func (s sortedEndpoints) Swap(i, j int) { |
| paddy@41 | 240 s[i], s[j] = s[j], s[i] |
| paddy@41 | 241 } |
| paddy@41 | 242 |
| paddy@57 | 243 type clientStore interface { |
| paddy@57 | 244 getClient(id uuid.ID) (Client, error) |
| paddy@57 | 245 saveClient(client Client) error |
| paddy@57 | 246 updateClient(id uuid.ID, change ClientChange) error |
| paddy@57 | 247 deleteClient(id uuid.ID) error |
| paddy@57 | 248 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) |
| paddy@41 | 249 |
| paddy@115 | 250 addEndpoints(client uuid.ID, endpoint []Endpoint) error |
| paddy@57 | 251 removeEndpoint(client, endpoint uuid.ID) error |
| paddy@58 | 252 checkEndpoint(client uuid.ID, endpoint string) (bool, error) |
| paddy@57 | 253 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) |
| paddy@57 | 254 countEndpoints(client uuid.ID) (int64, error) |
| paddy@0 | 255 } |
| paddy@31 | 256 |
| paddy@57 | 257 func (m *memstore) getClient(id uuid.ID) (Client, error) { |
| paddy@31 | 258 m.clientLock.RLock() |
| paddy@31 | 259 defer m.clientLock.RUnlock() |
| paddy@31 | 260 c, ok := m.clients[id.String()] |
| paddy@31 | 261 if !ok { |
| paddy@31 | 262 return Client{}, ErrClientNotFound |
| paddy@31 | 263 } |
| paddy@31 | 264 return c, nil |
| paddy@31 | 265 } |
| paddy@31 | 266 |
| paddy@57 | 267 func (m *memstore) saveClient(client Client) error { |
| paddy@31 | 268 m.clientLock.Lock() |
| paddy@31 | 269 defer m.clientLock.Unlock() |
| paddy@31 | 270 if _, ok := m.clients[client.ID.String()]; ok { |
| paddy@31 | 271 return ErrClientAlreadyExists |
| paddy@31 | 272 } |
| paddy@31 | 273 m.clients[client.ID.String()] = client |
| paddy@31 | 274 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID) |
| paddy@31 | 275 return nil |
| paddy@31 | 276 } |
| paddy@31 | 277 |
| paddy@57 | 278 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error { |
| paddy@39 | 279 m.clientLock.Lock() |
| paddy@39 | 280 defer m.clientLock.Unlock() |
| paddy@39 | 281 c, ok := m.clients[id.String()] |
| paddy@39 | 282 if !ok { |
| paddy@39 | 283 return ErrClientNotFound |
| paddy@39 | 284 } |
| paddy@39 | 285 c.ApplyChange(change) |
| paddy@39 | 286 m.clients[id.String()] = c |
| paddy@31 | 287 return nil |
| paddy@31 | 288 } |
| paddy@31 | 289 |
| paddy@57 | 290 func (m *memstore) deleteClient(id uuid.ID) error { |
| paddy@57 | 291 client, err := m.getClient(id) |
| paddy@31 | 292 if err != nil { |
| paddy@31 | 293 return err |
| paddy@31 | 294 } |
| paddy@31 | 295 m.clientLock.Lock() |
| paddy@31 | 296 defer m.clientLock.Unlock() |
| paddy@31 | 297 delete(m.clients, id.String()) |
| paddy@31 | 298 pos := -1 |
| paddy@31 | 299 for p, item := range m.profileClientLookup[client.OwnerID.String()] { |
| paddy@31 | 300 if item.Equal(id) { |
| paddy@31 | 301 pos = p |
| paddy@31 | 302 break |
| paddy@31 | 303 } |
| paddy@31 | 304 } |
| paddy@31 | 305 if pos >= 0 { |
| paddy@31 | 306 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...) |
| paddy@31 | 307 } |
| paddy@31 | 308 return nil |
| paddy@31 | 309 } |
| paddy@31 | 310 |
| paddy@57 | 311 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) { |
| paddy@33 | 312 ids := m.lookupClientsByProfileID(ownerID.String()) |
| paddy@31 | 313 if len(ids) > num+offset { |
| paddy@31 | 314 ids = ids[offset : num+offset] |
| paddy@31 | 315 } else if len(ids) > offset { |
| paddy@31 | 316 ids = ids[offset:] |
| paddy@31 | 317 } else { |
| paddy@31 | 318 return []Client{}, nil |
| paddy@31 | 319 } |
| paddy@31 | 320 clients := []Client{} |
| paddy@31 | 321 for _, id := range ids { |
| paddy@57 | 322 client, err := m.getClient(id) |
| paddy@31 | 323 if err != nil { |
| paddy@31 | 324 return []Client{}, err |
| paddy@31 | 325 } |
| paddy@31 | 326 clients = append(clients, client) |
| paddy@31 | 327 } |
| paddy@31 | 328 return clients, nil |
| paddy@31 | 329 } |
| paddy@41 | 330 |
| paddy@115 | 331 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error { |
| paddy@41 | 332 m.endpointLock.Lock() |
| paddy@41 | 333 defer m.endpointLock.Unlock() |
| paddy@115 | 334 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...) |
| paddy@41 | 335 return nil |
| paddy@41 | 336 } |
| paddy@41 | 337 |
| paddy@57 | 338 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error { |
| paddy@41 | 339 m.endpointLock.Lock() |
| paddy@41 | 340 defer m.endpointLock.Unlock() |
| paddy@41 | 341 pos := -1 |
| paddy@41 | 342 for p, item := range m.endpoints[client.String()] { |
| paddy@41 | 343 if item.ID.Equal(endpoint) { |
| paddy@41 | 344 pos = p |
| paddy@41 | 345 break |
| paddy@41 | 346 } |
| paddy@41 | 347 } |
| paddy@41 | 348 if pos >= 0 { |
| paddy@41 | 349 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...) |
| paddy@41 | 350 } |
| paddy@41 | 351 return nil |
| paddy@41 | 352 } |
| paddy@41 | 353 |
| paddy@58 | 354 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) { |
| paddy@41 | 355 m.endpointLock.RLock() |
| paddy@41 | 356 defer m.endpointLock.RUnlock() |
| paddy@41 | 357 for _, candidate := range m.endpoints[client.String()] { |
| paddy@116 | 358 if endpoint == candidate.NormalizedURI { |
| paddy@41 | 359 return true, nil |
| paddy@41 | 360 } |
| paddy@41 | 361 } |
| paddy@41 | 362 return false, nil |
| paddy@41 | 363 } |
| paddy@41 | 364 |
| paddy@57 | 365 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) { |
| paddy@41 | 366 m.endpointLock.RLock() |
| paddy@41 | 367 defer m.endpointLock.RUnlock() |
| paddy@41 | 368 return m.endpoints[client.String()], nil |
| paddy@41 | 369 } |
| paddy@54 | 370 |
| paddy@57 | 371 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) { |
| paddy@54 | 372 m.endpointLock.RLock() |
| paddy@54 | 373 defer m.endpointLock.RUnlock() |
| paddy@54 | 374 return int64(len(m.endpoints[client.String()])), nil |
| paddy@54 | 375 } |
| paddy@108 | 376 |
| paddy@108 | 377 type newClientReq struct { |
| paddy@108 | 378 Name string `json:"name"` |
| paddy@108 | 379 Logo string `json:"logo"` |
| paddy@108 | 380 Website string `json:"website"` |
| paddy@108 | 381 Type string `json:"type"` |
| paddy@108 | 382 Endpoints []string `json:"endpoints"` |
| paddy@108 | 383 } |
| paddy@108 | 384 |
| paddy@108 | 385 func RegisterClientHandlers(r *mux.Router, context Context) { |
| paddy@108 | 386 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST") |
| paddy@128 | 387 // BUG(paddy): We need to implement a handler to retrieve info on a client. |
| paddy@128 | 388 // BUG(paddy): We need to implement a handler to list clients. |
| paddy@128 | 389 // BUG(paddy): We need to implement a handler to update a client. |
| paddy@128 | 390 // 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 | 391 // BUG(paddy): We need to implement a handler to add an endpoint to a client. |
| paddy@128 | 392 // BUG(paddy): We need to implement a handler to remove an endpoint from a client. |
| paddy@128 | 393 // BUG(paddy): We need to implement a handler to list endpoints. |
| paddy@108 | 394 } |
| paddy@108 | 395 |
| paddy@108 | 396 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@115 | 397 errors := []requestError{} |
| paddy@108 | 398 username, password, ok := r.BasicAuth() |
| paddy@108 | 399 if !ok { |
| paddy@115 | 400 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@115 | 401 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@108 | 402 return |
| paddy@108 | 403 } |
| paddy@108 | 404 profile, err := authenticate(username, password, c) |
| paddy@108 | 405 if err != nil { |
| paddy@115 | 406 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@115 | 407 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@108 | 408 return |
| paddy@108 | 409 } |
| paddy@108 | 410 var req newClientReq |
| paddy@108 | 411 decoder := json.NewDecoder(r.Body) |
| paddy@108 | 412 err = decoder.Decode(&req) |
| paddy@108 | 413 if err != nil { |
| paddy@108 | 414 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@108 | 415 return |
| paddy@108 | 416 } |
| paddy@116 | 417 if req.Type == "" { |
| paddy@116 | 418 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"}) |
| paddy@116 | 419 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential { |
| paddy@115 | 420 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"}) |
| paddy@116 | 421 } |
| paddy@116 | 422 if req.Name == "" { |
| paddy@116 | 423 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"}) |
| paddy@116 | 424 } else if len(req.Name) < minClientNameLen { |
| paddy@116 | 425 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) |
| paddy@116 | 426 } else if len(req.Name) > maxClientNameLen { |
| paddy@116 | 427 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) |
| paddy@116 | 428 } |
| paddy@116 | 429 if len(errors) > 0 { |
| paddy@115 | 430 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@108 | 431 return |
| paddy@108 | 432 } |
| paddy@108 | 433 client := Client{ |
| paddy@108 | 434 ID: uuid.NewID(), |
| paddy@108 | 435 OwnerID: profile.ID, |
| paddy@108 | 436 Name: req.Name, |
| paddy@108 | 437 Logo: req.Logo, |
| paddy@108 | 438 Website: req.Website, |
| paddy@108 | 439 Type: req.Type, |
| paddy@108 | 440 } |
| paddy@118 | 441 if client.Type == clientTypeConfidential { |
| paddy@115 | 442 secret := make([]byte, 32) |
| paddy@115 | 443 _, err = rand.Read(secret) |
| paddy@115 | 444 if err != nil { |
| paddy@115 | 445 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@115 | 446 return |
| paddy@115 | 447 } |
| paddy@115 | 448 client.Secret = hex.EncodeToString(secret) |
| paddy@115 | 449 } |
| paddy@108 | 450 err = c.SaveClient(client) |
| paddy@108 | 451 if err != nil { |
| paddy@115 | 452 if err == ErrClientAlreadyExists { |
| paddy@115 | 453 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"}) |
| paddy@115 | 454 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@115 | 455 return |
| paddy@115 | 456 } |
| paddy@115 | 457 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@108 | 458 return |
| paddy@108 | 459 } |
| paddy@108 | 460 endpoints := []Endpoint{} |
| paddy@115 | 461 for pos, u := range req.Endpoints { |
| paddy@108 | 462 uri, err := url.Parse(u) |
| paddy@108 | 463 if err != nil { |
| paddy@115 | 464 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@108 | 465 continue |
| paddy@108 | 466 } |
| paddy@116 | 467 if !uri.IsAbs() { |
| paddy@116 | 468 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@116 | 469 continue |
| paddy@116 | 470 } |
| paddy@108 | 471 endpoint := Endpoint{ |
| paddy@108 | 472 ID: uuid.NewID(), |
| paddy@108 | 473 ClientID: client.ID, |
| paddy@116 | 474 URI: uri.String(), |
| paddy@108 | 475 Added: time.Now(), |
| paddy@108 | 476 } |
| paddy@108 | 477 endpoints = append(endpoints, endpoint) |
| paddy@108 | 478 } |
| paddy@115 | 479 err = c.AddEndpoints(client.ID, endpoints) |
| paddy@115 | 480 if err != nil { |
| paddy@115 | 481 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@115 | 482 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}}) |
| paddy@115 | 483 return |
| paddy@115 | 484 } |
| paddy@108 | 485 resp := response{ |
| paddy@108 | 486 Clients: []Client{client}, |
| paddy@108 | 487 Endpoints: endpoints, |
| paddy@116 | 488 Errors: errors, |
| paddy@108 | 489 } |
| paddy@108 | 490 encode(w, r, http.StatusCreated, resp) |
| paddy@108 | 491 } |
| paddy@121 | 492 |
| paddy@121 | 493 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) { |
| paddy@121 | 494 scope = r.PostFormValue("scope") |
| paddy@121 | 495 valid = true |
| paddy@121 | 496 return |
| paddy@121 | 497 } |
| paddy@124 | 498 |
| paddy@124 | 499 func clientCredentialsAuditString(r *http.Request) string { |
| paddy@124 | 500 return "client_credentials" |
| paddy@124 | 501 } |