Update to use a generic event emitter.
Rather can creating a purpose-built event emitter for each and every event we
need to emit (I'm looking at you, login verification event) which is _downright
silly_, we're now using a generic event publisher that's based on saying "HEY A
MODEL UPDATED".
This means we need to change all our setup code in authd to use
events.NewNSQPublisher or events.NewStdoutPublisher instead of our homegrown
solutions. Which also means updating our config to take an events.Publisher
instead of our LoginVerificationNotifier (blergh).
Our Context also now uses an events.Publisher instead of a
LoginVerificationNotifier. Party all around! We also replaced our
SendLoginVerification helper method on Context with a SendModelEvent helper
method on Context, which is just a light wrapper around
events.PublishModelEvent.
Of course, all this means we need to update our email_verification listener to
listen to the correct channel (based on the model we want updates about) and
filter down to a Created action or our new custom action for "the customer wants
their verification resent", which I'm OK making a special case and not generic,
because c'mon. But we had a subtle change to all our constants, some of which
are unofficial constants now. I'm unsure how I feel about this.
We also updated our email_verification listener so that we're unmarshalling to a
custom loginEvent, which is just an events.Event that overwrites the Data
property to be an auth.Login instance. This is to make sure we don't need to
wrangle a map[string]interface{}, which is no fun. I'm also OK with
special-casing like this, because it's 1) a tiny amount of code, 2) properly
utilising composition, and 3) the only way I can think of to cleanly accomplish
what I want.
I also added a note about GetLogin's deficient handling of logins, namely that
it doesn't recognise admins and return Verification codes to them, which would
be a useful property for internal tools to take advantage of. Ah well.
I updated the Profile and Login implementations so they're now event.Model
instances, mainly by just exporting some strings from them through getters that
will let us automatically build an Event from them. This lets us use the
PublishModelEvent helper.
I updated our CreateProfileHandler to properly mangle the login Verification
property, and to fire off the ActionCreated events for the new Login and the new
Profile.
I updated our GetLoginHandler and UpdateLoginHandler to properly mangle the
loginVerification property. God that's annoying. :-/
You'll note I didn't start publishing the events.ActionUpdated or
events.ActionDeleted events for Profiles or Logins yet, and didn't bother
publishing any events for literally any other type. That's because I'm a lazy
piece of crap and will end up publishing them when I absolutely have to. Part of
that is because if a channel isn't created/being read for a topic, the messages
will just stack up in NSQ, and I don't want that. But mostly I'm lazy.
Finally, I got to delete the entire profile_verification.go file, because we're
no longer special-casing that. Hooray!
12 "code.secondbit.org/events.hg"
13 "code.secondbit.org/uuid.hg"
15 "github.com/gorilla/mux"
19 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive.
20 MinPassphraseLength = 6
21 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive.
22 MaxPassphraseLength = 64
23 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme
24 CurPassphraseScheme = 1
25 // MaxNameLength is the maximum length, in bytes, of a name, exclusive.
27 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
30 // ActionResendVerification is the action property of the event sent when the user wants to resend their login verification code.
31 ActionResendVerification = "resend_verification"
35 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
36 ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
37 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
38 // the same ID already exists in the profileStore.
39 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
40 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
41 ErrProfileNotFound = errors.New("profile not found in profileStore")
42 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
43 // Type and Value already exists in the profileStore.
44 ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
45 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
46 ErrLoginNotFound = errors.New("login not found in profileStore")
47 // ErrLoginVerificationInvalid is returned when a Login is verified with the wrong verification code.
48 ErrLoginVerificationInvalid = errors.New("login verification code incorrect")
50 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
51 // Passphrase, and requires one.
52 ErrMissingPassphrase = errors.New("missing passphrase")
53 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
54 // a PassphraseReset, and requires one.
55 ErrMissingPassphraseReset = errors.New("missing passphrase reset")
56 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
57 // contain a PassphraseResetCreated, and requires one.
58 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
59 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
60 // but the Passphrase is shorter than MinPassphraseLength.
61 ErrPassphraseTooShort = errors.New("passphrase too short")
62 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
63 // but the Passphrase is longer than MaxPassphraseLength.
64 ErrPassphraseTooLong = errors.New("passphrase too long")
66 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected
67 // of being compromised.
68 ErrProfileCompromised = errors.New("profile compromised")
69 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain
70 // duration, to prevent brute force attacks.
71 ErrProfileLocked = errors.New("profile locked")
73 ScopeLoginAdmin = Scope{ID: "login_admin", Name: "Administer Logins", Description: "Read and write logins, bypassing ACL."}
76 // Profile represents a single user of the service,
77 // including their authentication information.
79 ID uuid.ID `json:"id,omitempty"`
80 Name string `json:"name,omitempty"`
81 Passphrase string `json:"-"`
82 Iterations int `json:"-"`
83 Salt string `json:"-"`
84 PassphraseScheme int `json:"-"`
85 Compromised bool `json:"-"`
86 LockedUntil time.Time `json:"-"`
87 PassphraseReset string `json:"-"`
88 PassphraseResetCreated time.Time `json:"-"`
89 Created time.Time `json:"created,omitempty"`
90 LastSeen time.Time `json:"last_seen,omitempty"`
93 func (p Profile) GetModelName() string {
97 func (p Profile) GetID() string {
101 func (p Profile) GetSystem() string {
102 return "code.secondbit.org/auth"
105 // ApplyChange applies the properties of the passed ProfileChange
106 // to the Profile it is called on.
107 func (p *Profile) ApplyChange(change ProfileChange) {
108 if change.Name != nil {
109 p.Name = *change.Name
111 if change.Passphrase != nil {
112 p.Passphrase = *change.Passphrase
114 if change.Iterations != nil {
115 p.Iterations = *change.Iterations
117 if change.Salt != nil {
118 p.Salt = *change.Salt
120 if change.PassphraseScheme != nil {
121 p.PassphraseScheme = *change.PassphraseScheme
123 if change.Compromised != nil {
124 p.Compromised = *change.Compromised
126 if change.LockedUntil != nil {
127 p.LockedUntil = *change.LockedUntil
129 if change.PassphraseReset != nil {
130 p.PassphraseReset = *change.PassphraseReset
132 if change.PassphraseResetCreated != nil {
133 p.PassphraseResetCreated = *change.PassphraseResetCreated
135 if change.LastSeen != nil {
136 p.LastSeen = *change.LastSeen
140 // ApplyBulkChange applies the properties of the passed BulkProfileChange
141 // to the Profile it is called on.
142 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
143 if change.Compromised != nil {
144 p.Compromised = *change.Compromised
148 // ProfileChange represents a single atomic change to a Profile's mutable data.
149 type ProfileChange struct {
154 PassphraseScheme *int
156 LockedUntil *time.Time
157 PassphraseReset *string
158 PassphraseResetCreated *time.Time
162 func (c ProfileChange) Empty() bool {
163 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)
166 // Validate checks the ProfileChange it is called on
167 // and asserts its internal validity, or lack thereof.
168 // A descriptive error will be returned in the case of
169 // an invalid change.
170 func (c ProfileChange) Validate() error {
172 return ErrEmptyChange
174 if c.PassphraseScheme != nil && c.Passphrase == nil {
175 return ErrMissingPassphrase
177 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
178 return ErrMissingPassphraseResetCreated
180 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
181 return ErrMissingPassphraseReset
183 if c.Salt != nil && c.Passphrase == nil {
184 return ErrMissingPassphrase
186 if c.Iterations != nil && c.Passphrase == nil {
187 return ErrMissingPassphrase
192 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
193 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
194 // ProfileChange values across many Profiles all at once.
195 type BulkProfileChange struct {
199 func (b BulkProfileChange) Empty() bool {
200 return b.Compromised == nil
203 // Validate checks the BulkProfileChange it is called on
204 // and asserts its internal validity, or lack thereof.
205 // A descriptive error will be returned in the case of an
207 func (b BulkProfileChange) Validate() error {
209 return ErrEmptyChange
214 // Login represents a single human-friendly identifier for
215 // a given Profile that can be used to log into that Profile.
216 // Each Profile may only have one Login for each Type.
218 Type string `json:"type,omitempty"`
219 Value string `json:"value,omitempty"`
220 ProfileID uuid.ID `json:"profile_id,omitempty"`
221 Created time.Time `json:"created,omitempty"`
222 LastUsed time.Time `json:"last_used,omitempty"`
223 Verification string `json:"verification,omitempty"`
224 Verified bool `json:"verified"`
227 func (l Login) GetModelName() string {
231 func (l Login) GetID() string {
235 func (l Login) GetSystem() string {
236 return "code.secondbit.org/auth"
239 type LoginChange struct {
240 Verification *string `json:"verification,omitempty"`
241 ResendVerification *bool `json:"resend_verification,omitempty"`
244 type newProfileRequest struct {
245 Email string `json:"email"`
246 Passphrase string `json:"passphrase"`
247 Name string `json:"name"`
250 func validateNewProfileRequest(req *newProfileRequest) []RequestError {
251 errors := []RequestError{}
252 req.Name = strings.TrimSpace(req.Name)
253 req.Email = strings.TrimSpace(req.Email)
254 if len(req.Passphrase) < MinPassphraseLength {
255 errors = append(errors, RequestError{
256 Slug: RequestErrInsufficient,
257 Field: "/passphrase",
260 if len(req.Passphrase) > MaxPassphraseLength {
261 errors = append(errors, RequestError{
262 Slug: RequestErrOverflow,
263 Field: "/passphrase",
266 if len(req.Name) > MaxNameLength {
267 errors = append(errors, RequestError{
268 Slug: RequestErrOverflow,
273 errors = append(errors, RequestError{
274 Slug: RequestErrMissing,
278 if len(req.Email) > MaxEmailLength {
279 errors = append(errors, RequestError{
280 Slug: RequestErrOverflow,
284 re := regexp.MustCompile(".+@.+\\..+")
285 if !re.Match([]byte(req.Email)) {
286 errors = append(errors, RequestError{
287 Slug: RequestErrInvalidFormat,
294 type profileStore interface {
295 getProfileByID(id uuid.ID) (Profile, error)
296 getProfileByLogin(value string) (Profile, error)
297 saveProfile(profile Profile) error
298 updateProfile(id uuid.ID, change ProfileChange) error
299 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
300 deleteProfile(id uuid.ID) error
302 addLogin(login Login) error
303 getLogin(value string) (Login, error)
304 removeLogin(value string, profile uuid.ID) error
305 removeLoginsByProfile(profile uuid.ID) error
306 recordLoginUse(value string, when time.Time) error
307 verifyLogin(value, verification string) error
308 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
311 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
312 m.profileLock.RLock()
313 defer m.profileLock.RUnlock()
314 p, ok := m.profiles[id.String()]
316 return Profile{}, ErrProfileNotFound
321 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
323 defer m.loginLock.RUnlock()
324 login, ok := m.logins[value]
326 return Profile{}, ErrLoginNotFound
328 m.profileLock.RLock()
329 defer m.profileLock.RUnlock()
330 profile, ok := m.profiles[login.ProfileID.String()]
332 return Profile{}, ErrProfileNotFound
337 func (m *memstore) saveProfile(profile Profile) error {
339 defer m.profileLock.Unlock()
340 _, ok := m.profiles[profile.ID.String()]
342 return ErrProfileAlreadyExists
344 m.profiles[profile.ID.String()] = profile
348 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
350 defer m.profileLock.Unlock()
351 p, ok := m.profiles[id.String()]
353 return ErrProfileNotFound
355 p.ApplyChange(change)
356 m.profiles[id.String()] = p
360 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
362 defer m.profileLock.Unlock()
363 for id, profile := range m.profiles {
364 for _, i := range ids {
365 if id == i.String() {
366 profile.ApplyBulkChange(change)
367 m.profiles[id] = profile
375 func (m *memstore) deleteProfile(id uuid.ID) error {
377 defer m.profileLock.Unlock()
378 if _, ok := m.profiles[id.String()]; !ok {
379 return ErrProfileNotFound
381 delete(m.profiles, id.String())
385 func (m *memstore) addLogin(login Login) error {
387 defer m.loginLock.Unlock()
388 _, ok := m.logins[login.Value]
390 return ErrLoginAlreadyExists
392 m.logins[login.Value] = login
393 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
397 func (m *memstore) getLogin(value string) (Login, error) {
399 defer m.loginLock.RUnlock()
400 l, ok := m.logins[value]
402 return Login{}, ErrLoginNotFound
407 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
409 defer m.loginLock.Unlock()
410 l, ok := m.logins[value]
412 return ErrLoginNotFound
414 if !l.ProfileID.Equal(profile) {
415 return ErrLoginNotFound
417 delete(m.logins, value)
419 for p, id := range m.profileLoginLookup[profile.String()] {
426 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
431 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
433 defer m.loginLock.Unlock()
434 logins, ok := m.profileLoginLookup[profile.String()]
436 return ErrProfileNotFound
438 delete(m.profileLoginLookup, profile.String())
439 for _, login := range logins {
440 delete(m.logins, login)
445 func (m *memstore) recordLoginUse(value string, when time.Time) error {
447 defer m.loginLock.Unlock()
448 l, ok := m.logins[value]
450 return ErrLoginNotFound
457 func (m *memstore) verifyLogin(value, verification string) error {
459 defer m.loginLock.Unlock()
460 l, ok := m.logins[value]
462 return ErrLoginNotFound
464 if l.Verification != verification {
465 return ErrLoginVerificationInvalid
472 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
474 defer m.loginLock.RUnlock()
475 ids, ok := m.profileLoginLookup[profile.String()]
477 return []Login{}, nil
479 if len(ids) > num+offset {
480 ids = ids[offset : num+offset]
481 } else if len(ids) > offset {
484 return []Login{}, nil
487 for _, id := range ids {
488 login, ok := m.logins[id]
492 logins = append(logins, login)
497 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
498 err := context.RemoveLoginsByProfile(profile)
500 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
502 err = context.TerminateSessionsByProfile(profile)
504 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
506 err = context.RevokeTokensByProfileID(profile)
508 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
510 err = context.DeleteAuthorizationCodesByProfileID(profile)
512 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
514 clients, err := context.ListClientsByOwner(profile, -1, 0)
516 log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
518 err = context.DeleteClientsByOwner(profile)
520 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
522 for _, client := range clients {
523 cleanUpAfterClientDeletion(client.ID, context)
527 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
528 func RegisterProfileHandlers(r *mux.Router, context Context) {
529 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS")
530 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS")
531 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS")
532 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS")
533 // BUG(paddy): We need to implement a handler that will add a login to a profile.
534 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login?
535 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
536 r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS")
537 r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS")
540 // GetProfileHandler is an HTTP handler for retrieving a profile.
541 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
542 errors := []RequestError{}
543 authz := r.Header.Get("Authorization")
544 if !strings.HasPrefix(authz, "Bearer ") {
545 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
546 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
549 authz = strings.TrimPrefix(authz, "Bearer ")
551 if vars["id"] == "" {
552 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
553 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
556 id, err := uuid.Parse(vars["id"])
558 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
559 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
562 token, err := context.GetToken(authz, false)
563 if err != nil || token.Revoked {
564 if err == ErrTokenNotFound || token.Revoked {
565 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
566 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
569 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
573 if !id.Equal(token.ProfileID) {
574 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
575 encode(w, r, http.StatusForbidden, Response{Errors: errors})
578 profile, err := context.GetProfileByID(id)
580 if err == ErrProfileNotFound {
581 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
582 encode(w, r, http.StatusNotFound, Response{Errors: errors})
585 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
588 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
592 // CreateProfileHandler is an HTTP handler for registering new profiles.
593 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
594 scheme, ok := passphraseSchemes[CurPassphraseScheme]
596 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
597 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
600 var req newProfileRequest
601 errors := []RequestError{}
602 decoder := json.NewDecoder(r.Body)
603 err := decoder.Decode(&req)
605 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
608 errors = append(errors, validateNewProfileRequest(&req)...)
610 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
613 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
615 log.Printf("Error creating encoded passphrase: %#+v\n", err)
616 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
622 Passphrase: string(passphrase),
623 Iterations: context.config.iterations,
625 PassphraseScheme: CurPassphraseScheme,
627 LastSeen: time.Now(),
629 err = context.SaveProfile(profile)
631 if err == ErrProfileAlreadyExists {
632 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}})
635 log.Printf("Error saving profile: %#+v\n", err)
636 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
643 Created: profile.Created,
644 LastUsed: profile.Created,
645 ProfileID: profile.ID,
646 Verification: uuid.NewID().String(),
648 err = context.AddLogin(login)
650 if err == ErrLoginAlreadyExists {
651 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}})
654 log.Printf("Error adding login: %#+v\n", err)
655 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
658 verification := login.Verification
659 login.Verification = "" // clear verification so it's not exposed
660 logins = append(logins, login)
663 Profiles: []Profile{profile},
665 encode(w, r, http.StatusCreated, resp)
666 login.Verification = verification // restore verification so it's included in the event
667 go context.SendModelEvent(login, events.ActionCreated)
668 go context.SendModelEvent(profile, events.ActionCreated)
671 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
672 errors := []RequestError{}
674 if vars["id"] == "" {
675 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
676 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
679 id, err := uuid.Parse(vars["id"])
681 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
682 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
685 username, password, ok := r.BasicAuth()
687 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
688 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
691 profile, err := authenticate(username, password, context)
693 if isAuthError(err) {
694 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
695 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
697 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
698 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
702 if !profile.ID.Equal(id) {
703 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
704 encode(w, r, http.StatusForbidden, Response{Errors: errors})
707 var req ProfileChange
708 decoder := json.NewDecoder(r.Body)
709 err = decoder.Decode(&req)
711 log.Printf("Error decoding request: %#+v\n", err)
712 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
717 req.PassphraseScheme = nil
718 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
719 req.LockedUntil = nil
721 if req.Passphrase != nil {
722 if len(*req.Passphrase) < MinPassphraseLength {
723 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"})
724 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
727 if len(*req.Passphrase) > MaxPassphraseLength {
728 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"})
729 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
732 iterations := context.config.iterations
733 scheme, ok := passphraseSchemes[CurPassphraseScheme]
735 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
738 curScheme := CurPassphraseScheme
739 req.PassphraseScheme = &curScheme
740 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
742 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
745 req.Passphrase = &passphrase
747 req.Iterations = &iterations
749 if req.PassphraseReset != nil {
751 req.PassphraseResetCreated = &now
759 resp.Profiles = []Profile{profile}
760 status = http.StatusOK
762 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
764 status = http.StatusInternalServerError
766 encode(w, r, status, resp)
769 err = context.UpdateProfile(id, req)
771 if err == ErrProfileNotFound {
772 errors = append(errors, RequestError{Slug: RequestErrNotFound})
773 encode(w, r, http.StatusNotFound, Response{Errors: errors})
776 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
779 profile.ApplyChange(req)
780 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
784 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
785 errors := []RequestError{}
787 if vars["id"] == "" {
788 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
789 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
792 id, err := uuid.Parse(vars["id"])
794 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
795 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
798 username, password, ok := r.BasicAuth()
800 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
801 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
804 profile, err := authenticate(username, password, context)
806 if isAuthError(err) {
807 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
808 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
810 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
811 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
815 if !profile.ID.Equal(id) {
816 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
817 encode(w, r, http.StatusForbidden, Response{Errors: errors})
820 err = context.DeleteProfile(id)
822 if err == ErrProfileNotFound {
823 errors = append(errors, RequestError{Slug: RequestErrNotFound})
824 encode(w, r, http.StatusNotFound, Response{Errors: errors})
827 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
830 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
831 go cleanUpAfterProfileDeletion(profile.ID, context)
834 func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
835 var errors []RequestError
837 if vars["login"] == "" {
838 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
839 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
842 login, err := context.GetLogin(vars["login"])
844 if err == ErrLoginNotFound {
845 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
846 encode(w, r, http.StatusNotFound, Response{Errors: errors})
849 log.Printf("Error retrieving login: %#+v\n", err)
850 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
853 // clear verification code so it's not exposed
854 // BUG(paddy): We hsould only hide the verification code if it's not an admin request, but auth isn't set up properly for scopes yet
855 login.Verification = ""
856 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
859 func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
860 var errors []RequestError
862 if vars["login"] == "" {
863 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
864 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
868 decoder := json.NewDecoder(r.Body)
869 err := decoder.Decode(&req)
871 log.Printf("Error decoding request: %#+v\n", err)
872 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
875 login, err := context.GetLogin(vars["login"])
877 if err == ErrLoginNotFound {
878 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
879 encode(w, r, http.StatusNotFound, Response{Errors: errors})
882 log.Printf("Error retrieving login: %#+v\n", err)
883 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
886 if req.Verification != nil {
887 err = context.VerifyLogin(vars["login"], *req.Verification)
889 if err == ErrLoginNotFound {
890 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
891 encode(w, r, http.StatusNotFound, Response{Errors: errors})
893 } else if err == ErrLoginVerificationInvalid {
894 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"})
895 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
898 log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err)
899 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
902 login.Verified = true
903 } else if req.ResendVerification != nil {
904 if !*req.ResendVerification {
905 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"})
906 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
909 go context.SendModelEvent(login, ActionResendVerification)
911 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
912 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
915 // clear the Verification code so it's not exposed
916 login.Verification = ""
917 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})