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