auth

Paddy 2015-05-17 Parent:581c60f8dd23 Child:b0d1b3e39fc8

172:8ecb60d29b0d Go to Latest

auth/profile.go

Support email verification. The bulk of this commit is auto-modifying files to export variables (mostly our request error types and our response type) so that they can be reused in a Go client for that API. We also implement the beginnings of a Go client for that API, implementing the bare minimum we need for our immediate purposes: the ability to retrieve information about a Login. This, of course, means we need an API endpoint that will return information about a Login, which in turn required us to implement a GetLogin method in our profileStore. Which got in-memory and postgres implementations. That done, we could add the Verification field and Verified field to the Login type, to keep track of whether we've verified the user's ownership of those communication methods (if the Login is, in fact, a communication method). This required us to update sql/postgres_init.sql to account for the new fields we're tracking. It also means that when creating a Login, we had to generate a UUID to use as the Verification field. To make things complete, we needed a verifyLogin method on the profileStore to mark a Login as verified. That, in turn, required an endpoint to control this through the API. While doing so, I lumped things together in an UpdateLogin handler just so we could reuse the endpoint and logic when resending a verification email that may have never reached the user, for whatever reason (the quintessential "send again" button). Finally, we implemented an email_verification listener that will pull email_verification events off NSQ, check for the requisite data integrity, and use mailgun to email out a verification/welcome email.

History
1 package auth
3 import (
4 "encoding/json"
5 "errors"
6 "log"
7 "net/http"
8 "regexp"
9 "strings"
10 "time"
12 "code.secondbit.org/uuid.hg"
13 "github.com/gorilla/mux"
14 )
16 const (
17 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive.
18 MinPassphraseLength = 6
19 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive.
20 MaxPassphraseLength = 64
21 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme
22 CurPassphraseScheme = 1
23 // MaxNameLength is the maximum length, in bytes, of a name, exclusive.
24 MaxNameLength = 64
25 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
26 MaxEmailLength = 64
27 )
29 var (
30 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
31 ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
32 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
33 // the same ID already exists in the profileStore.
34 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
35 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
36 ErrProfileNotFound = errors.New("profile not found in profileStore")
37 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
38 // Type and Value already exists in the profileStore.
39 ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
40 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
41 ErrLoginNotFound = errors.New("login not found in profileStore")
42 // ErrLoginVerificationInvalid is returned when a Login is verified with the wrong verification code.
43 ErrLoginVerificationInvalid = errors.New("login verification code incorrect")
45 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
46 // Passphrase, and requires one.
47 ErrMissingPassphrase = errors.New("missing passphrase")
48 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
49 // a PassphraseReset, and requires one.
50 ErrMissingPassphraseReset = errors.New("missing passphrase reset")
51 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
52 // contain a PassphraseResetCreated, and requires one.
53 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
54 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
55 // but the Passphrase is shorter than MinPassphraseLength.
56 ErrPassphraseTooShort = errors.New("passphrase too short")
57 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
58 // but the Passphrase is longer than MaxPassphraseLength.
59 ErrPassphraseTooLong = errors.New("passphrase too long")
61 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected
62 // of being compromised.
63 ErrProfileCompromised = errors.New("profile compromised")
64 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain
65 // duration, to prevent brute force attacks.
66 ErrProfileLocked = errors.New("profile locked")
67 )
69 // Profile represents a single user of the service,
70 // including their authentication information.
71 type Profile struct {
72 ID uuid.ID `json:"id,omitempty"`
73 Name string `json:"name,omitempty"`
74 Passphrase string `json:"-"`
75 Iterations int `json:"-"`
76 Salt string `json:"-"`
77 PassphraseScheme int `json:"-"`
78 Compromised bool `json:"-"`
79 LockedUntil time.Time `json:"-"`
80 PassphraseReset string `json:"-"`
81 PassphraseResetCreated time.Time `json:"-"`
82 Created time.Time `json:"created,omitempty"`
83 LastSeen time.Time `json:"last_seen,omitempty"`
84 }
86 // ApplyChange applies the properties of the passed ProfileChange
87 // to the Profile it is called on.
88 func (p *Profile) ApplyChange(change ProfileChange) {
89 if change.Name != nil {
90 p.Name = *change.Name
91 }
92 if change.Passphrase != nil {
93 p.Passphrase = *change.Passphrase
94 }
95 if change.Iterations != nil {
96 p.Iterations = *change.Iterations
97 }
98 if change.Salt != nil {
99 p.Salt = *change.Salt
100 }
101 if change.PassphraseScheme != nil {
102 p.PassphraseScheme = *change.PassphraseScheme
103 }
104 if change.Compromised != nil {
105 p.Compromised = *change.Compromised
106 }
107 if change.LockedUntil != nil {
108 p.LockedUntil = *change.LockedUntil
109 }
110 if change.PassphraseReset != nil {
111 p.PassphraseReset = *change.PassphraseReset
112 }
113 if change.PassphraseResetCreated != nil {
114 p.PassphraseResetCreated = *change.PassphraseResetCreated
115 }
116 if change.LastSeen != nil {
117 p.LastSeen = *change.LastSeen
118 }
119 }
121 // ApplyBulkChange applies the properties of the passed BulkProfileChange
122 // to the Profile it is called on.
123 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
124 if change.Compromised != nil {
125 p.Compromised = *change.Compromised
126 }
127 }
129 // ProfileChange represents a single atomic change to a Profile's mutable data.
130 type ProfileChange struct {
131 Name *string
132 Passphrase *string
133 Iterations *int
134 Salt *string
135 PassphraseScheme *int
136 Compromised *bool
137 LockedUntil *time.Time
138 PassphraseReset *string
139 PassphraseResetCreated *time.Time
140 LastSeen *time.Time
141 }
143 func (c ProfileChange) Empty() bool {
144 return (c.Name == nil && c.Passphrase == nil && c.Iterations == nil && c.Salt == nil && c.PassphraseScheme == nil && c.Compromised == nil && c.LockedUntil == nil && c.PassphraseReset == nil && c.PassphraseResetCreated == nil && c.LastSeen == nil)
145 }
147 // Validate checks the ProfileChange it is called on
148 // and asserts its internal validity, or lack thereof.
149 // A descriptive error will be returned in the case of
150 // an invalid change.
151 func (c ProfileChange) Validate() error {
152 if c.Empty() {
153 return ErrEmptyChange
154 }
155 if c.PassphraseScheme != nil && c.Passphrase == nil {
156 return ErrMissingPassphrase
157 }
158 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
159 return ErrMissingPassphraseResetCreated
160 }
161 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
162 return ErrMissingPassphraseReset
163 }
164 if c.Salt != nil && c.Passphrase == nil {
165 return ErrMissingPassphrase
166 }
167 if c.Iterations != nil && c.Passphrase == nil {
168 return ErrMissingPassphrase
169 }
170 return nil
171 }
173 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
174 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
175 // ProfileChange values across many Profiles all at once.
176 type BulkProfileChange struct {
177 Compromised *bool
178 }
180 func (b BulkProfileChange) Empty() bool {
181 return b.Compromised == nil
182 }
184 // Validate checks the BulkProfileChange it is called on
185 // and asserts its internal validity, or lack thereof.
186 // A descriptive error will be returned in the case of an
187 // invalid change.
188 func (b BulkProfileChange) Validate() error {
189 if b.Empty() {
190 return ErrEmptyChange
191 }
192 return nil
193 }
195 // Login represents a single human-friendly identifier for
196 // a given Profile that can be used to log into that Profile.
197 // Each Profile may only have one Login for each Type.
198 type Login struct {
199 Type string `json:"type,omitempty"`
200 Value string `json:"value,omitempty"`
201 ProfileID uuid.ID `json:"profile_id,omitempty"`
202 Created time.Time `json:"created,omitempty"`
203 LastUsed time.Time `json:"last_used,omitempty"`
204 Verification string `json:"-"`
205 Verified bool `json:"verified"`
206 }
208 type LoginChange struct {
209 Verification *string `json:"verification,omitempty"`
210 ResendVerification *bool `json:"resend_verification,omitempty"`
211 }
213 type newProfileRequest struct {
214 Email string `json:"email"`
215 Passphrase string `json:"passphrase"`
216 Name string `json:"name"`
217 }
219 func validateNewProfileRequest(req *newProfileRequest) []RequestError {
220 errors := []RequestError{}
221 req.Name = strings.TrimSpace(req.Name)
222 req.Email = strings.TrimSpace(req.Email)
223 if len(req.Passphrase) < MinPassphraseLength {
224 errors = append(errors, RequestError{
225 Slug: RequestErrInsufficient,
226 Field: "/passphrase",
227 })
228 }
229 if len(req.Passphrase) > MaxPassphraseLength {
230 errors = append(errors, RequestError{
231 Slug: RequestErrOverflow,
232 Field: "/passphrase",
233 })
234 }
235 if len(req.Name) > MaxNameLength {
236 errors = append(errors, RequestError{
237 Slug: RequestErrOverflow,
238 Field: "/name",
239 })
240 }
241 if req.Email == "" {
242 errors = append(errors, RequestError{
243 Slug: RequestErrMissing,
244 Field: "/email",
245 })
246 }
247 if len(req.Email) > MaxEmailLength {
248 errors = append(errors, RequestError{
249 Slug: RequestErrOverflow,
250 Field: "/email",
251 })
252 }
253 re := regexp.MustCompile(".+@.+\\..+")
254 if !re.Match([]byte(req.Email)) {
255 errors = append(errors, RequestError{
256 Slug: RequestErrInvalidFormat,
257 Field: "/email",
258 })
259 }
260 return errors
261 }
263 type profileStore interface {
264 getProfileByID(id uuid.ID) (Profile, error)
265 getProfileByLogin(value string) (Profile, error)
266 saveProfile(profile Profile) error
267 updateProfile(id uuid.ID, change ProfileChange) error
268 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
269 deleteProfile(id uuid.ID) error
271 addLogin(login Login) error
272 getLogin(value string) (Login, error)
273 removeLogin(value string, profile uuid.ID) error
274 removeLoginsByProfile(profile uuid.ID) error
275 recordLoginUse(value string, when time.Time) error
276 verifyLogin(value, verification string) error
277 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
278 }
280 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
281 m.profileLock.RLock()
282 defer m.profileLock.RUnlock()
283 p, ok := m.profiles[id.String()]
284 if !ok {
285 return Profile{}, ErrProfileNotFound
286 }
287 return p, nil
288 }
290 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
291 m.loginLock.RLock()
292 defer m.loginLock.RUnlock()
293 login, ok := m.logins[value]
294 if !ok {
295 return Profile{}, ErrLoginNotFound
296 }
297 m.profileLock.RLock()
298 defer m.profileLock.RUnlock()
299 profile, ok := m.profiles[login.ProfileID.String()]
300 if !ok {
301 return Profile{}, ErrProfileNotFound
302 }
303 return profile, nil
304 }
306 func (m *memstore) saveProfile(profile Profile) error {
307 m.profileLock.Lock()
308 defer m.profileLock.Unlock()
309 _, ok := m.profiles[profile.ID.String()]
310 if ok {
311 return ErrProfileAlreadyExists
312 }
313 m.profiles[profile.ID.String()] = profile
314 return nil
315 }
317 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
318 m.profileLock.Lock()
319 defer m.profileLock.Unlock()
320 p, ok := m.profiles[id.String()]
321 if !ok {
322 return ErrProfileNotFound
323 }
324 p.ApplyChange(change)
325 m.profiles[id.String()] = p
326 return nil
327 }
329 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
330 m.profileLock.Lock()
331 defer m.profileLock.Unlock()
332 for id, profile := range m.profiles {
333 for _, i := range ids {
334 if id == i.String() {
335 profile.ApplyBulkChange(change)
336 m.profiles[id] = profile
337 break
338 }
339 }
340 }
341 return nil
342 }
344 func (m *memstore) deleteProfile(id uuid.ID) error {
345 m.profileLock.Lock()
346 defer m.profileLock.Unlock()
347 if _, ok := m.profiles[id.String()]; !ok {
348 return ErrProfileNotFound
349 }
350 delete(m.profiles, id.String())
351 return nil
352 }
354 func (m *memstore) addLogin(login Login) error {
355 m.loginLock.Lock()
356 defer m.loginLock.Unlock()
357 _, ok := m.logins[login.Value]
358 if ok {
359 return ErrLoginAlreadyExists
360 }
361 m.logins[login.Value] = login
362 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
363 return nil
364 }
366 func (m *memstore) getLogin(value string) (Login, error) {
367 m.loginLock.RLock()
368 defer m.loginLock.RUnlock()
369 l, ok := m.logins[value]
370 if !ok {
371 return Login{}, ErrLoginNotFound
372 }
373 return l, nil
374 }
376 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
377 m.loginLock.Lock()
378 defer m.loginLock.Unlock()
379 l, ok := m.logins[value]
380 if !ok {
381 return ErrLoginNotFound
382 }
383 if !l.ProfileID.Equal(profile) {
384 return ErrLoginNotFound
385 }
386 delete(m.logins, value)
387 pos := -1
388 for p, id := range m.profileLoginLookup[profile.String()] {
389 if id == value {
390 pos = p
391 break
392 }
393 }
394 if pos >= 0 {
395 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
396 }
397 return nil
398 }
400 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
401 m.loginLock.Lock()
402 defer m.loginLock.Unlock()
403 logins, ok := m.profileLoginLookup[profile.String()]
404 if !ok {
405 return ErrProfileNotFound
406 }
407 delete(m.profileLoginLookup, profile.String())
408 for _, login := range logins {
409 delete(m.logins, login)
410 }
411 return nil
412 }
414 func (m *memstore) recordLoginUse(value string, when time.Time) error {
415 m.loginLock.Lock()
416 defer m.loginLock.Unlock()
417 l, ok := m.logins[value]
418 if !ok {
419 return ErrLoginNotFound
420 }
421 l.LastUsed = when
422 m.logins[value] = l
423 return nil
424 }
426 func (m *memstore) verifyLogin(value, verification string) error {
427 m.loginLock.Lock()
428 defer m.loginLock.Unlock()
429 l, ok := m.logins[value]
430 if !ok {
431 return ErrLoginNotFound
432 }
433 if l.Verification != verification {
434 return ErrLoginVerificationInvalid
435 }
436 l.Verified = true
437 m.logins[value] = l
438 return nil
439 }
441 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
442 m.loginLock.RLock()
443 defer m.loginLock.RUnlock()
444 ids, ok := m.profileLoginLookup[profile.String()]
445 if !ok {
446 return []Login{}, nil
447 }
448 if len(ids) > num+offset {
449 ids = ids[offset : num+offset]
450 } else if len(ids) > offset {
451 ids = ids[offset:]
452 } else {
453 return []Login{}, nil
454 }
455 logins := []Login{}
456 for _, id := range ids {
457 login, ok := m.logins[id]
458 if !ok {
459 continue
460 }
461 logins = append(logins, login)
462 }
463 return logins, nil
464 }
466 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
467 err := context.RemoveLoginsByProfile(profile)
468 if err != nil {
469 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
470 }
471 err = context.TerminateSessionsByProfile(profile)
472 if err != nil {
473 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
474 }
475 err = context.RevokeTokensByProfileID(profile)
476 if err != nil {
477 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
478 }
479 err = context.DeleteAuthorizationCodesByProfileID(profile)
480 if err != nil {
481 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
482 }
483 clients, err := context.ListClientsByOwner(profile, -1, 0)
484 if err != nil {
485 log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
486 }
487 err = context.DeleteClientsByOwner(profile)
488 if err != nil {
489 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
490 }
491 for _, client := range clients {
492 cleanUpAfterClientDeletion(client.ID, context)
493 }
494 }
496 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
497 func RegisterProfileHandlers(r *mux.Router, context Context) {
498 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS")
499 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS")
500 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS")
501 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS")
502 // BUG(paddy): We need to implement a handler that will add a login to a profile.
503 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login?
504 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
505 r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS")
506 r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS")
507 }
509 // GetProfileHandler is an HTTP handler for retrieving a profile.
510 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
511 errors := []RequestError{}
512 authz := r.Header.Get("Authorization")
513 if !strings.HasPrefix(authz, "Bearer ") {
514 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
515 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
516 return
517 }
518 authz = strings.TrimPrefix(authz, "Bearer ")
519 vars := mux.Vars(r)
520 if vars["id"] == "" {
521 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
522 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
523 return
524 }
525 id, err := uuid.Parse(vars["id"])
526 if err != nil {
527 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
528 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
529 return
530 }
531 token, err := context.GetToken(authz, false)
532 if err != nil || token.Revoked {
533 if err == ErrTokenNotFound || token.Revoked {
534 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
535 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
536 return
537 } else {
538 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
539 return
540 }
541 }
542 if !id.Equal(token.ProfileID) {
543 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
544 encode(w, r, http.StatusForbidden, Response{Errors: errors})
545 return
546 }
547 profile, err := context.GetProfileByID(id)
548 if err != nil {
549 if err == ErrProfileNotFound {
550 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
551 encode(w, r, http.StatusNotFound, Response{Errors: errors})
552 return
553 }
554 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
555 return
556 }
557 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
558 return
559 }
561 // CreateProfileHandler is an HTTP handler for registering new profiles.
562 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
563 scheme, ok := passphraseSchemes[CurPassphraseScheme]
564 if !ok {
565 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
566 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
567 return
568 }
569 var req newProfileRequest
570 errors := []RequestError{}
571 decoder := json.NewDecoder(r.Body)
572 err := decoder.Decode(&req)
573 if err != nil {
574 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
575 return
576 }
577 errors = append(errors, validateNewProfileRequest(&req)...)
578 if len(errors) > 0 {
579 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
580 return
581 }
582 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
583 if err != nil {
584 log.Printf("Error creating encoded passphrase: %#+v\n", err)
585 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
586 return
587 }
588 profile := Profile{
589 ID: uuid.NewID(),
590 Name: req.Name,
591 Passphrase: string(passphrase),
592 Iterations: context.config.iterations,
593 Salt: string(salt),
594 PassphraseScheme: CurPassphraseScheme,
595 Created: time.Now(),
596 LastSeen: time.Now(),
597 }
598 err = context.SaveProfile(profile)
599 if err != nil {
600 if err == ErrProfileAlreadyExists {
601 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}})
602 return
603 }
604 log.Printf("Error saving profile: %#+v\n", err)
605 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
606 return
607 }
608 logins := []Login{}
609 login := Login{
610 Type: "email",
611 Value: req.Email,
612 Created: profile.Created,
613 LastUsed: profile.Created,
614 ProfileID: profile.ID,
615 Verification: uuid.NewID().String(),
616 }
617 err = context.AddLogin(login)
618 if err != nil {
619 if err == ErrLoginAlreadyExists {
620 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}})
621 return
622 }
623 log.Printf("Error adding login: %#+v\n", err)
624 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
625 return
626 }
627 logins = append(logins, login)
628 resp := Response{
629 Logins: logins,
630 Profiles: []Profile{profile},
631 }
632 encode(w, r, http.StatusCreated, resp)
633 go context.SendLoginVerification(login)
634 }
636 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
637 errors := []RequestError{}
638 vars := mux.Vars(r)
639 if vars["id"] == "" {
640 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
641 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
642 return
643 }
644 id, err := uuid.Parse(vars["id"])
645 if err != nil {
646 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
647 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
648 return
649 }
650 username, password, ok := r.BasicAuth()
651 if !ok {
652 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
653 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
654 return
655 }
656 profile, err := authenticate(username, password, context)
657 if err != nil {
658 if isAuthError(err) {
659 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
660 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
661 } else {
662 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
663 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
664 }
665 return
666 }
667 if !profile.ID.Equal(id) {
668 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
669 encode(w, r, http.StatusForbidden, Response{Errors: errors})
670 return
671 }
672 var req ProfileChange
673 decoder := json.NewDecoder(r.Body)
674 err = decoder.Decode(&req)
675 if err != nil {
676 log.Printf("Error decoding request: %#+v\n", err)
677 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
678 return
679 }
680 req.Iterations = nil
681 req.Salt = nil
682 req.PassphraseScheme = nil
683 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
684 req.LockedUntil = nil
685 req.LastSeen = nil
686 if req.Passphrase != nil {
687 if len(*req.Passphrase) < MinPassphraseLength {
688 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"})
689 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
690 return
691 }
692 if len(*req.Passphrase) > MaxPassphraseLength {
693 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"})
694 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
695 return
696 }
697 iterations := context.config.iterations
698 scheme, ok := passphraseSchemes[CurPassphraseScheme]
699 if !ok {
700 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
701 return
702 }
703 curScheme := CurPassphraseScheme
704 req.PassphraseScheme = &curScheme
705 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
706 if err != nil {
707 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
708 return
709 }
710 req.Passphrase = &passphrase
711 req.Salt = &salt
712 req.Iterations = &iterations
713 }
714 if req.PassphraseReset != nil {
715 now := time.Now()
716 req.PassphraseResetCreated = &now
717 }
718 err = req.Validate()
719 if err != nil {
720 var status int
721 var resp Response
722 switch err {
723 case ErrEmptyChange:
724 resp.Profiles = []Profile{profile}
725 status = http.StatusOK
726 default:
727 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
728 resp.Errors = errors
729 status = http.StatusInternalServerError
730 }
731 encode(w, r, status, resp)
732 return
733 }
734 err = context.UpdateProfile(id, req)
735 if err != nil {
736 if err == ErrProfileNotFound {
737 errors = append(errors, RequestError{Slug: RequestErrNotFound})
738 encode(w, r, http.StatusNotFound, Response{Errors: errors})
739 return
740 }
741 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
742 return
743 }
744 profile.ApplyChange(req)
745 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
746 return
747 }
749 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
750 errors := []RequestError{}
751 vars := mux.Vars(r)
752 if vars["id"] == "" {
753 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
754 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
755 return
756 }
757 id, err := uuid.Parse(vars["id"])
758 if err != nil {
759 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
760 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
761 return
762 }
763 username, password, ok := r.BasicAuth()
764 if !ok {
765 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
766 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
767 return
768 }
769 profile, err := authenticate(username, password, context)
770 if err != nil {
771 if isAuthError(err) {
772 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
773 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
774 } else {
775 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
776 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
777 }
778 return
779 }
780 if !profile.ID.Equal(id) {
781 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
782 encode(w, r, http.StatusForbidden, Response{Errors: errors})
783 return
784 }
785 err = context.DeleteProfile(id)
786 if err != nil {
787 if err == ErrProfileNotFound {
788 errors = append(errors, RequestError{Slug: RequestErrNotFound})
789 encode(w, r, http.StatusNotFound, Response{Errors: errors})
790 return
791 }
792 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
793 return
794 }
795 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
796 go cleanUpAfterProfileDeletion(profile.ID, context)
797 }
799 func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
800 var errors []RequestError
801 vars := mux.Vars(r)
802 if vars["login"] == "" {
803 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
804 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
805 return
806 }
807 login, err := context.GetLogin(vars["login"])
808 if err != nil {
809 if err == ErrLoginNotFound {
810 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
811 encode(w, r, http.StatusNotFound, Response{Errors: errors})
812 return
813 }
814 log.Printf("Error retrieving login: %#+v\n", err)
815 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
816 return
817 }
818 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
819 }
821 func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
822 var errors []RequestError
823 vars := mux.Vars(r)
824 if vars["login"] == "" {
825 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
826 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
827 return
828 }
829 var req LoginChange
830 decoder := json.NewDecoder(r.Body)
831 err := decoder.Decode(&req)
832 if err != nil {
833 log.Printf("Error decoding request: %#+v\n", err)
834 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
835 return
836 }
837 login, err := context.GetLogin(vars["login"])
838 if err != nil {
839 if err == ErrLoginNotFound {
840 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
841 encode(w, r, http.StatusNotFound, Response{Errors: errors})
842 return
843 }
844 log.Printf("Error retrieving login: %#+v\n", err)
845 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
846 return
847 }
848 if req.Verification != nil {
849 err = context.VerifyLogin(vars["login"], *req.Verification)
850 if err != nil {
851 if err == ErrLoginNotFound {
852 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
853 encode(w, r, http.StatusNotFound, Response{Errors: errors})
854 return
855 } else if err == ErrLoginVerificationInvalid {
856 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"})
857 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
858 return
859 }
860 log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err)
861 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
862 return
863 }
864 login.Verified = true
865 } else if req.ResendVerification != nil {
866 if !*req.ResendVerification {
867 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"})
868 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
869 return
870 }
871 context.SendLoginVerification(login)
872 } else {
873 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
874 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
875 return
876 }
877 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
878 }