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