Break out scopes and events.
This repo has gotten unwieldy, and there are portions of it that need to be
imported by a large number of other packages.
For example, scopes will be used in almost every API we write. Rather than
importing the entirety of this codebase into every API we write, I've opted to
move the scope logic out into a scopes package, with a subpackage for the
defined types, which is all most projects actually want to import.
We also define some event type constants, and importing those shouldn't require
a project to import all our dependencies, either. So I made an events subpackage
that just holds those constants.
This package has become a little bit of a red-headed stepchild and is do for a
refactor, but I'm trying to put that off as long as I can.
The refactoring of our scopes stuff has left a bug wherein a token can be
granted for scopes that don't exist. I'm going to need to revisit that, and also
how to limit scopes to only be granted to the users that should be able to
request them. But that's a battle for another day.
12 "code.secondbit.org/auth.hg/events"
13 "code.secondbit.org/events.hg"
14 "code.secondbit.org/scopes.hg/types"
15 "code.secondbit.org/uuid.hg"
17 "github.com/gorilla/mux"
21 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive.
22 MinPassphraseLength = 6
23 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive.
24 MaxPassphraseLength = 64
25 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme
26 CurPassphraseScheme = 1
27 // MaxNameLength is the maximum length, in bytes, of a name, exclusive.
29 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
34 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
35 ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
36 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
37 // the same ID already exists in the profileStore.
38 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
39 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
40 ErrProfileNotFound = errors.New("profile not found in profileStore")
41 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
42 // Type and Value already exists in the profileStore.
43 ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
44 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
45 ErrLoginNotFound = errors.New("login not found in profileStore")
46 // ErrLoginVerificationInvalid is returned when a Login is verified with the wrong verification code.
47 ErrLoginVerificationInvalid = errors.New("login verification code incorrect")
49 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
50 // Passphrase, and requires one.
51 ErrMissingPassphrase = errors.New("missing passphrase")
52 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
53 // a PassphraseReset, and requires one.
54 ErrMissingPassphraseReset = errors.New("missing passphrase reset")
55 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
56 // contain a PassphraseResetCreated, and requires one.
57 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
58 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
59 // but the Passphrase is shorter than MinPassphraseLength.
60 ErrPassphraseTooShort = errors.New("passphrase too short")
61 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
62 // but the Passphrase is longer than MaxPassphraseLength.
63 ErrPassphraseTooLong = errors.New("passphrase too long")
65 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected
66 // of being compromised.
67 ErrProfileCompromised = errors.New("profile compromised")
68 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain
69 // duration, to prevent brute force attacks.
70 ErrProfileLocked = errors.New("profile locked")
72 ScopeLoginAdmin = scopeTypes.Scope{ID: "login_admin", Name: "Administer Logins", Description: "Read and write logins, bypassing ACL."}
75 // Profile represents a single user of the service,
76 // including their authentication information.
78 ID uuid.ID `json:"id,omitempty"`
79 Name string `json:"name,omitempty"`
80 Passphrase string `json:"-"`
81 Iterations int `json:"-"`
82 Salt string `json:"-"`
83 PassphraseScheme int `json:"-"`
84 Compromised bool `json:"-"`
85 LockedUntil time.Time `json:"-"`
86 PassphraseReset string `json:"-"`
87 PassphraseResetCreated time.Time `json:"-"`
88 Created time.Time `json:"created,omitempty"`
89 LastSeen time.Time `json:"last_seen,omitempty"`
92 func (p Profile) GetModelName() string {
96 func (p Profile) GetID() string {
100 func (p Profile) GetSystem() string {
101 return "code.secondbit.org/auth"
104 // ApplyChange applies the properties of the passed ProfileChange
105 // to the Profile it is called on.
106 func (p *Profile) ApplyChange(change ProfileChange) {
107 if change.Name != nil {
108 p.Name = *change.Name
110 if change.Passphrase != nil {
111 p.Passphrase = *change.Passphrase
113 if change.Iterations != nil {
114 p.Iterations = *change.Iterations
116 if change.Salt != nil {
117 p.Salt = *change.Salt
119 if change.PassphraseScheme != nil {
120 p.PassphraseScheme = *change.PassphraseScheme
122 if change.Compromised != nil {
123 p.Compromised = *change.Compromised
125 if change.LockedUntil != nil {
126 p.LockedUntil = *change.LockedUntil
128 if change.PassphraseReset != nil {
129 p.PassphraseReset = *change.PassphraseReset
131 if change.PassphraseResetCreated != nil {
132 p.PassphraseResetCreated = *change.PassphraseResetCreated
134 if change.LastSeen != nil {
135 p.LastSeen = *change.LastSeen
139 // ApplyBulkChange applies the properties of the passed BulkProfileChange
140 // to the Profile it is called on.
141 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
142 if change.Compromised != nil {
143 p.Compromised = *change.Compromised
147 // ProfileChange represents a single atomic change to a Profile's mutable data.
148 type ProfileChange struct {
153 PassphraseScheme *int
155 LockedUntil *time.Time
156 PassphraseReset *string
157 PassphraseResetCreated *time.Time
161 func (c ProfileChange) Empty() bool {
162 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)
165 // Validate checks the ProfileChange it is called on
166 // and asserts its internal validity, or lack thereof.
167 // A descriptive error will be returned in the case of
168 // an invalid change.
169 func (c ProfileChange) Validate() error {
171 return ErrEmptyChange
173 if c.PassphraseScheme != nil && c.Passphrase == nil {
174 return ErrMissingPassphrase
176 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
177 return ErrMissingPassphraseResetCreated
179 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
180 return ErrMissingPassphraseReset
182 if c.Salt != nil && c.Passphrase == nil {
183 return ErrMissingPassphrase
185 if c.Iterations != nil && c.Passphrase == nil {
186 return ErrMissingPassphrase
191 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
192 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
193 // ProfileChange values across many Profiles all at once.
194 type BulkProfileChange struct {
198 func (b BulkProfileChange) Empty() bool {
199 return b.Compromised == nil
202 // Validate checks the BulkProfileChange it is called on
203 // and asserts its internal validity, or lack thereof.
204 // A descriptive error will be returned in the case of an
206 func (b BulkProfileChange) Validate() error {
208 return ErrEmptyChange
213 // Login represents a single human-friendly identifier for
214 // a given Profile that can be used to log into that Profile.
215 // Each Profile may only have one Login for each Type.
217 Type string `json:"type,omitempty"`
218 Value string `json:"value,omitempty"`
219 ProfileID uuid.ID `json:"profile_id,omitempty"`
220 Created time.Time `json:"created,omitempty"`
221 LastUsed time.Time `json:"last_used,omitempty"`
222 Verification string `json:"verification,omitempty"`
223 Verified bool `json:"verified"`
226 func (l Login) GetModelName() string {
230 func (l Login) GetID() string {
234 func (l Login) GetSystem() string {
235 return "code.secondbit.org/auth"
238 type LoginChange struct {
239 Verification *string `json:"verification,omitempty"`
240 ResendVerification *bool `json:"resend_verification,omitempty"`
243 type newProfileRequest struct {
244 Email string `json:"email"`
245 Passphrase string `json:"passphrase"`
246 Name string `json:"name"`
249 func validateNewProfileRequest(req *newProfileRequest) []RequestError {
250 errors := []RequestError{}
251 req.Name = strings.TrimSpace(req.Name)
252 req.Email = strings.TrimSpace(req.Email)
253 if len(req.Passphrase) < MinPassphraseLength {
254 errors = append(errors, RequestError{
255 Slug: RequestErrInsufficient,
256 Field: "/passphrase",
259 if len(req.Passphrase) > MaxPassphraseLength {
260 errors = append(errors, RequestError{
261 Slug: RequestErrOverflow,
262 Field: "/passphrase",
265 if len(req.Name) > MaxNameLength {
266 errors = append(errors, RequestError{
267 Slug: RequestErrOverflow,
272 errors = append(errors, RequestError{
273 Slug: RequestErrMissing,
277 if len(req.Email) > MaxEmailLength {
278 errors = append(errors, RequestError{
279 Slug: RequestErrOverflow,
283 re := regexp.MustCompile(".+@.+\\..+")
284 if !re.Match([]byte(req.Email)) {
285 errors = append(errors, RequestError{
286 Slug: RequestErrInvalidFormat,
293 type profileStore interface {
294 getProfileByID(id uuid.ID) (Profile, error)
295 getProfileByLogin(value string) (Profile, error)
296 saveProfile(profile Profile) error
297 updateProfile(id uuid.ID, change ProfileChange) error
298 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
299 deleteProfile(id uuid.ID) error
301 addLogin(login Login) error
302 getLogin(value string) (Login, error)
303 removeLogin(value string, profile uuid.ID) error
304 removeLoginsByProfile(profile uuid.ID) error
305 recordLoginUse(value string, when time.Time) error
306 verifyLogin(value, verification string) error
307 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
310 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
311 m.profileLock.RLock()
312 defer m.profileLock.RUnlock()
313 p, ok := m.profiles[id.String()]
315 return Profile{}, ErrProfileNotFound
320 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
322 defer m.loginLock.RUnlock()
323 login, ok := m.logins[value]
325 return Profile{}, ErrLoginNotFound
327 m.profileLock.RLock()
328 defer m.profileLock.RUnlock()
329 profile, ok := m.profiles[login.ProfileID.String()]
331 return Profile{}, ErrProfileNotFound
336 func (m *memstore) saveProfile(profile Profile) error {
338 defer m.profileLock.Unlock()
339 _, ok := m.profiles[profile.ID.String()]
341 return ErrProfileAlreadyExists
343 m.profiles[profile.ID.String()] = profile
347 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
349 defer m.profileLock.Unlock()
350 p, ok := m.profiles[id.String()]
352 return ErrProfileNotFound
354 p.ApplyChange(change)
355 m.profiles[id.String()] = p
359 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
361 defer m.profileLock.Unlock()
362 for id, profile := range m.profiles {
363 for _, i := range ids {
364 if id == i.String() {
365 profile.ApplyBulkChange(change)
366 m.profiles[id] = profile
374 func (m *memstore) deleteProfile(id uuid.ID) error {
376 defer m.profileLock.Unlock()
377 if _, ok := m.profiles[id.String()]; !ok {
378 return ErrProfileNotFound
380 delete(m.profiles, id.String())
384 func (m *memstore) addLogin(login Login) error {
386 defer m.loginLock.Unlock()
387 _, ok := m.logins[login.Value]
389 return ErrLoginAlreadyExists
391 m.logins[login.Value] = login
392 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
396 func (m *memstore) getLogin(value string) (Login, error) {
398 defer m.loginLock.RUnlock()
399 l, ok := m.logins[value]
401 return Login{}, ErrLoginNotFound
406 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
408 defer m.loginLock.Unlock()
409 l, ok := m.logins[value]
411 return ErrLoginNotFound
413 if !l.ProfileID.Equal(profile) {
414 return ErrLoginNotFound
416 delete(m.logins, value)
418 for p, id := range m.profileLoginLookup[profile.String()] {
425 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
430 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
432 defer m.loginLock.Unlock()
433 logins, ok := m.profileLoginLookup[profile.String()]
435 return ErrProfileNotFound
437 delete(m.profileLoginLookup, profile.String())
438 for _, login := range logins {
439 delete(m.logins, login)
444 func (m *memstore) recordLoginUse(value string, when time.Time) error {
446 defer m.loginLock.Unlock()
447 l, ok := m.logins[value]
449 return ErrLoginNotFound
456 func (m *memstore) verifyLogin(value, verification string) error {
458 defer m.loginLock.Unlock()
459 l, ok := m.logins[value]
461 return ErrLoginNotFound
463 if l.Verification != verification {
464 return ErrLoginVerificationInvalid
471 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
473 defer m.loginLock.RUnlock()
474 ids, ok := m.profileLoginLookup[profile.String()]
476 return []Login{}, nil
478 if len(ids) > num+offset {
479 ids = ids[offset : num+offset]
480 } else if len(ids) > offset {
483 return []Login{}, nil
486 for _, id := range ids {
487 login, ok := m.logins[id]
491 logins = append(logins, login)
496 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
497 err := context.RemoveLoginsByProfile(profile)
499 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
501 err = context.TerminateSessionsByProfile(profile)
503 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
505 err = context.RevokeTokensByProfileID(profile)
507 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
509 err = context.DeleteAuthorizationCodesByProfileID(profile)
511 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
513 clients, err := context.ListClientsByOwner(profile, -1, 0)
515 log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
517 err = context.DeleteClientsByOwner(profile)
519 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
521 for _, client := range clients {
522 cleanUpAfterClientDeletion(client.ID, context)
526 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
527 func RegisterProfileHandlers(r *mux.Router, context Context) {
528 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS")
529 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS")
530 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS")
531 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS")
532 // BUG(paddy): We need to implement a handler that will add a login to a profile.
533 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login?
534 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
535 r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS")
536 r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS")
539 // GetProfileHandler is an HTTP handler for retrieving a profile.
540 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
541 errors := []RequestError{}
542 authz := r.Header.Get("Authorization")
543 if !strings.HasPrefix(authz, "Bearer ") {
544 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
545 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
548 authz = strings.TrimPrefix(authz, "Bearer ")
550 if vars["id"] == "" {
551 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
552 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
555 id, err := uuid.Parse(vars["id"])
557 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
558 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
561 token, err := context.GetToken(authz, false)
562 if err != nil || token.Revoked {
563 if err == ErrTokenNotFound || token.Revoked {
564 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
565 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
568 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
572 if !id.Equal(token.ProfileID) {
573 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
574 encode(w, r, http.StatusForbidden, Response{Errors: errors})
577 profile, err := context.GetProfileByID(id)
579 if err == ErrProfileNotFound {
580 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
581 encode(w, r, http.StatusNotFound, Response{Errors: errors})
584 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
587 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
591 // CreateProfileHandler is an HTTP handler for registering new profiles.
592 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
593 scheme, ok := passphraseSchemes[CurPassphraseScheme]
595 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
596 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
599 var req newProfileRequest
600 errors := []RequestError{}
601 decoder := json.NewDecoder(r.Body)
602 err := decoder.Decode(&req)
604 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
607 errors = append(errors, validateNewProfileRequest(&req)...)
609 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
612 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
614 log.Printf("Error creating encoded passphrase: %#+v\n", err)
615 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
621 Passphrase: string(passphrase),
622 Iterations: context.config.iterations,
624 PassphraseScheme: CurPassphraseScheme,
626 LastSeen: time.Now(),
628 err = context.SaveProfile(profile)
630 if err == ErrProfileAlreadyExists {
631 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}})
634 log.Printf("Error saving profile: %#+v\n", err)
635 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
642 Created: profile.Created,
643 LastUsed: profile.Created,
644 ProfileID: profile.ID,
645 Verification: uuid.NewID().String(),
647 err = context.AddLogin(login)
649 if err == ErrLoginAlreadyExists {
650 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}})
653 log.Printf("Error adding login: %#+v\n", err)
654 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
657 verification := login.Verification
658 login.Verification = "" // clear verification so it's not exposed
659 logins = append(logins, login)
662 Profiles: []Profile{profile},
664 encode(w, r, http.StatusCreated, resp)
665 login.Verification = verification // restore verification so it's included in the event
666 go context.SendModelEvent(login, events.ActionCreated)
667 go context.SendModelEvent(profile, events.ActionCreated)
670 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
671 errors := []RequestError{}
673 if vars["id"] == "" {
674 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
675 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
678 id, err := uuid.Parse(vars["id"])
680 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
681 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
684 username, password, ok := r.BasicAuth()
686 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
687 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
690 profile, err := authenticate(username, password, context)
692 if isAuthError(err) {
693 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
694 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
696 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
697 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
701 if !profile.ID.Equal(id) {
702 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
703 encode(w, r, http.StatusForbidden, Response{Errors: errors})
706 var req ProfileChange
707 decoder := json.NewDecoder(r.Body)
708 err = decoder.Decode(&req)
710 log.Printf("Error decoding request: %#+v\n", err)
711 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
716 req.PassphraseScheme = nil
717 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
718 req.LockedUntil = nil
720 if req.Passphrase != nil {
721 if len(*req.Passphrase) < MinPassphraseLength {
722 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"})
723 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
726 if len(*req.Passphrase) > MaxPassphraseLength {
727 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"})
728 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
731 iterations := context.config.iterations
732 scheme, ok := passphraseSchemes[CurPassphraseScheme]
734 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
737 curScheme := CurPassphraseScheme
738 req.PassphraseScheme = &curScheme
739 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
741 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
744 req.Passphrase = &passphrase
746 req.Iterations = &iterations
748 if req.PassphraseReset != nil {
750 req.PassphraseResetCreated = &now
758 resp.Profiles = []Profile{profile}
759 status = http.StatusOK
761 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
763 status = http.StatusInternalServerError
765 encode(w, r, status, resp)
768 err = context.UpdateProfile(id, req)
770 if err == ErrProfileNotFound {
771 errors = append(errors, RequestError{Slug: RequestErrNotFound})
772 encode(w, r, http.StatusNotFound, Response{Errors: errors})
775 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
778 profile.ApplyChange(req)
779 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
783 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
784 errors := []RequestError{}
786 if vars["id"] == "" {
787 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
788 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
791 id, err := uuid.Parse(vars["id"])
793 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
794 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
797 username, password, ok := r.BasicAuth()
799 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
800 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
803 profile, err := authenticate(username, password, context)
805 if isAuthError(err) {
806 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
807 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
809 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
810 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
814 if !profile.ID.Equal(id) {
815 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
816 encode(w, r, http.StatusForbidden, Response{Errors: errors})
819 err = context.DeleteProfile(id)
821 if err == ErrProfileNotFound {
822 errors = append(errors, RequestError{Slug: RequestErrNotFound})
823 encode(w, r, http.StatusNotFound, Response{Errors: errors})
826 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
829 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
830 go cleanUpAfterProfileDeletion(profile.ID, context)
833 func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
834 var errors []RequestError
836 if vars["login"] == "" {
837 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
838 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
841 login, err := context.GetLogin(vars["login"])
843 if err == ErrLoginNotFound {
844 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
845 encode(w, r, http.StatusNotFound, Response{Errors: errors})
848 log.Printf("Error retrieving login: %#+v\n", err)
849 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
852 // clear verification code so it's not exposed
853 // 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
854 login.Verification = ""
855 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
858 func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
859 var errors []RequestError
861 if vars["login"] == "" {
862 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
863 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
867 decoder := json.NewDecoder(r.Body)
868 err := decoder.Decode(&req)
870 log.Printf("Error decoding request: %#+v\n", err)
871 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
874 login, err := context.GetLogin(vars["login"])
876 if err == ErrLoginNotFound {
877 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
878 encode(w, r, http.StatusNotFound, Response{Errors: errors})
881 log.Printf("Error retrieving login: %#+v\n", err)
882 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
885 if req.Verification != nil {
886 err = context.VerifyLogin(vars["login"], *req.Verification)
888 if err == ErrLoginNotFound {
889 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
890 encode(w, r, http.StatusNotFound, Response{Errors: errors})
892 } else if err == ErrLoginVerificationInvalid {
893 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"})
894 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
897 log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err)
898 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
901 go context.SendModelEvent(login, authEvents.ActionLoginVerified)
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, authEvents.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}})