auth

Paddy 2015-12-14 Parent:7bba108d2d9a

181:b7e685839a1b Go to Latest

auth/profile.go

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.

History
1 package auth
3 import (
4 "encoding/json"
5 "errors"
6 "log"
7 "net/http"
8 "regexp"
9 "strings"
10 "time"
12 "code.secondbit.org/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"
18 )
20 const (
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.
28 MaxNameLength = 64
29 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
30 MaxEmailLength = 64
31 )
33 var (
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."}
73 )
75 // Profile represents a single user of the service,
76 // including their authentication information.
77 type Profile struct {
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"`
90 }
92 func (p Profile) GetModelName() string {
93 return "profiles"
94 }
96 func (p Profile) GetID() string {
97 return p.ID.String()
98 }
100 func (p Profile) GetSystem() string {
101 return "code.secondbit.org/auth"
102 }
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
109 }
110 if change.Passphrase != nil {
111 p.Passphrase = *change.Passphrase
112 }
113 if change.Iterations != nil {
114 p.Iterations = *change.Iterations
115 }
116 if change.Salt != nil {
117 p.Salt = *change.Salt
118 }
119 if change.PassphraseScheme != nil {
120 p.PassphraseScheme = *change.PassphraseScheme
121 }
122 if change.Compromised != nil {
123 p.Compromised = *change.Compromised
124 }
125 if change.LockedUntil != nil {
126 p.LockedUntil = *change.LockedUntil
127 }
128 if change.PassphraseReset != nil {
129 p.PassphraseReset = *change.PassphraseReset
130 }
131 if change.PassphraseResetCreated != nil {
132 p.PassphraseResetCreated = *change.PassphraseResetCreated
133 }
134 if change.LastSeen != nil {
135 p.LastSeen = *change.LastSeen
136 }
137 }
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
144 }
145 }
147 // ProfileChange represents a single atomic change to a Profile's mutable data.
148 type ProfileChange struct {
149 Name *string
150 Passphrase *string
151 Iterations *int
152 Salt *string
153 PassphraseScheme *int
154 Compromised *bool
155 LockedUntil *time.Time
156 PassphraseReset *string
157 PassphraseResetCreated *time.Time
158 LastSeen *time.Time
159 }
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)
163 }
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 {
170 if c.Empty() {
171 return ErrEmptyChange
172 }
173 if c.PassphraseScheme != nil && c.Passphrase == nil {
174 return ErrMissingPassphrase
175 }
176 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
177 return ErrMissingPassphraseResetCreated
178 }
179 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
180 return ErrMissingPassphraseReset
181 }
182 if c.Salt != nil && c.Passphrase == nil {
183 return ErrMissingPassphrase
184 }
185 if c.Iterations != nil && c.Passphrase == nil {
186 return ErrMissingPassphrase
187 }
188 return nil
189 }
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 {
195 Compromised *bool
196 }
198 func (b BulkProfileChange) Empty() bool {
199 return b.Compromised == nil
200 }
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
205 // invalid change.
206 func (b BulkProfileChange) Validate() error {
207 if b.Empty() {
208 return ErrEmptyChange
209 }
210 return nil
211 }
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.
216 type Login struct {
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"`
224 }
226 func (l Login) GetModelName() string {
227 return "logins"
228 }
230 func (l Login) GetID() string {
231 return l.Value
232 }
234 func (l Login) GetSystem() string {
235 return "code.secondbit.org/auth"
236 }
238 type LoginChange struct {
239 Verification *string `json:"verification,omitempty"`
240 ResendVerification *bool `json:"resend_verification,omitempty"`
241 }
243 type newProfileRequest struct {
244 Email string `json:"email"`
245 Passphrase string `json:"passphrase"`
246 Name string `json:"name"`
247 }
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",
257 })
258 }
259 if len(req.Passphrase) > MaxPassphraseLength {
260 errors = append(errors, RequestError{
261 Slug: RequestErrOverflow,
262 Field: "/passphrase",
263 })
264 }
265 if len(req.Name) > MaxNameLength {
266 errors = append(errors, RequestError{
267 Slug: RequestErrOverflow,
268 Field: "/name",
269 })
270 }
271 if req.Email == "" {
272 errors = append(errors, RequestError{
273 Slug: RequestErrMissing,
274 Field: "/email",
275 })
276 }
277 if len(req.Email) > MaxEmailLength {
278 errors = append(errors, RequestError{
279 Slug: RequestErrOverflow,
280 Field: "/email",
281 })
282 }
283 re := regexp.MustCompile(".+@.+\\..+")
284 if !re.Match([]byte(req.Email)) {
285 errors = append(errors, RequestError{
286 Slug: RequestErrInvalidFormat,
287 Field: "/email",
288 })
289 }
290 return errors
291 }
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)
308 }
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()]
314 if !ok {
315 return Profile{}, ErrProfileNotFound
316 }
317 return p, nil
318 }
320 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
321 m.loginLock.RLock()
322 defer m.loginLock.RUnlock()
323 login, ok := m.logins[value]
324 if !ok {
325 return Profile{}, ErrLoginNotFound
326 }
327 m.profileLock.RLock()
328 defer m.profileLock.RUnlock()
329 profile, ok := m.profiles[login.ProfileID.String()]
330 if !ok {
331 return Profile{}, ErrProfileNotFound
332 }
333 return profile, nil
334 }
336 func (m *memstore) saveProfile(profile Profile) error {
337 m.profileLock.Lock()
338 defer m.profileLock.Unlock()
339 _, ok := m.profiles[profile.ID.String()]
340 if ok {
341 return ErrProfileAlreadyExists
342 }
343 m.profiles[profile.ID.String()] = profile
344 return nil
345 }
347 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
348 m.profileLock.Lock()
349 defer m.profileLock.Unlock()
350 p, ok := m.profiles[id.String()]
351 if !ok {
352 return ErrProfileNotFound
353 }
354 p.ApplyChange(change)
355 m.profiles[id.String()] = p
356 return nil
357 }
359 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
360 m.profileLock.Lock()
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
367 break
368 }
369 }
370 }
371 return nil
372 }
374 func (m *memstore) deleteProfile(id uuid.ID) error {
375 m.profileLock.Lock()
376 defer m.profileLock.Unlock()
377 if _, ok := m.profiles[id.String()]; !ok {
378 return ErrProfileNotFound
379 }
380 delete(m.profiles, id.String())
381 return nil
382 }
384 func (m *memstore) addLogin(login Login) error {
385 m.loginLock.Lock()
386 defer m.loginLock.Unlock()
387 _, ok := m.logins[login.Value]
388 if ok {
389 return ErrLoginAlreadyExists
390 }
391 m.logins[login.Value] = login
392 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
393 return nil
394 }
396 func (m *memstore) getLogin(value string) (Login, error) {
397 m.loginLock.RLock()
398 defer m.loginLock.RUnlock()
399 l, ok := m.logins[value]
400 if !ok {
401 return Login{}, ErrLoginNotFound
402 }
403 return l, nil
404 }
406 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
407 m.loginLock.Lock()
408 defer m.loginLock.Unlock()
409 l, ok := m.logins[value]
410 if !ok {
411 return ErrLoginNotFound
412 }
413 if !l.ProfileID.Equal(profile) {
414 return ErrLoginNotFound
415 }
416 delete(m.logins, value)
417 pos := -1
418 for p, id := range m.profileLoginLookup[profile.String()] {
419 if id == value {
420 pos = p
421 break
422 }
423 }
424 if pos >= 0 {
425 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
426 }
427 return nil
428 }
430 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
431 m.loginLock.Lock()
432 defer m.loginLock.Unlock()
433 logins, ok := m.profileLoginLookup[profile.String()]
434 if !ok {
435 return ErrProfileNotFound
436 }
437 delete(m.profileLoginLookup, profile.String())
438 for _, login := range logins {
439 delete(m.logins, login)
440 }
441 return nil
442 }
444 func (m *memstore) recordLoginUse(value string, when time.Time) error {
445 m.loginLock.Lock()
446 defer m.loginLock.Unlock()
447 l, ok := m.logins[value]
448 if !ok {
449 return ErrLoginNotFound
450 }
451 l.LastUsed = when
452 m.logins[value] = l
453 return nil
454 }
456 func (m *memstore) verifyLogin(value, verification string) error {
457 m.loginLock.Lock()
458 defer m.loginLock.Unlock()
459 l, ok := m.logins[value]
460 if !ok {
461 return ErrLoginNotFound
462 }
463 if l.Verification != verification {
464 return ErrLoginVerificationInvalid
465 }
466 l.Verified = true
467 m.logins[value] = l
468 return nil
469 }
471 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
472 m.loginLock.RLock()
473 defer m.loginLock.RUnlock()
474 ids, ok := m.profileLoginLookup[profile.String()]
475 if !ok {
476 return []Login{}, nil
477 }
478 if len(ids) > num+offset {
479 ids = ids[offset : num+offset]
480 } else if len(ids) > offset {
481 ids = ids[offset:]
482 } else {
483 return []Login{}, nil
484 }
485 logins := []Login{}
486 for _, id := range ids {
487 login, ok := m.logins[id]
488 if !ok {
489 continue
490 }
491 logins = append(logins, login)
492 }
493 return logins, nil
494 }
496 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
497 err := context.RemoveLoginsByProfile(profile)
498 if err != nil {
499 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
500 }
501 err = context.TerminateSessionsByProfile(profile)
502 if err != nil {
503 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
504 }
505 err = context.RevokeTokensByProfileID(profile)
506 if err != nil {
507 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
508 }
509 err = context.DeleteAuthorizationCodesByProfileID(profile)
510 if err != nil {
511 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
512 }
513 clients, err := context.ListClientsByOwner(profile, -1, 0)
514 if err != nil {
515 log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
516 }
517 err = context.DeleteClientsByOwner(profile)
518 if err != nil {
519 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
520 }
521 for _, client := range clients {
522 cleanUpAfterClientDeletion(client.ID, context)
523 }
524 }
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")
537 }
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})
546 return
547 }
548 authz = strings.TrimPrefix(authz, "Bearer ")
549 vars := mux.Vars(r)
550 if vars["id"] == "" {
551 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
552 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
553 return
554 }
555 id, err := uuid.Parse(vars["id"])
556 if err != nil {
557 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
558 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
559 return
560 }
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})
566 return
567 } else {
568 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
569 return
570 }
571 }
572 if !id.Equal(token.ProfileID) {
573 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
574 encode(w, r, http.StatusForbidden, Response{Errors: errors})
575 return
576 }
577 profile, err := context.GetProfileByID(id)
578 if err != nil {
579 if err == ErrProfileNotFound {
580 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
581 encode(w, r, http.StatusNotFound, Response{Errors: errors})
582 return
583 }
584 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
585 return
586 }
587 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
588 return
589 }
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]
594 if !ok {
595 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
596 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
597 return
598 }
599 var req newProfileRequest
600 errors := []RequestError{}
601 decoder := json.NewDecoder(r.Body)
602 err := decoder.Decode(&req)
603 if err != nil {
604 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
605 return
606 }
607 errors = append(errors, validateNewProfileRequest(&req)...)
608 if len(errors) > 0 {
609 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
610 return
611 }
612 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
613 if err != nil {
614 log.Printf("Error creating encoded passphrase: %#+v\n", err)
615 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
616 return
617 }
618 profile := Profile{
619 ID: uuid.NewID(),
620 Name: req.Name,
621 Passphrase: string(passphrase),
622 Iterations: context.config.iterations,
623 Salt: string(salt),
624 PassphraseScheme: CurPassphraseScheme,
625 Created: time.Now(),
626 LastSeen: time.Now(),
627 }
628 err = context.SaveProfile(profile)
629 if err != nil {
630 if err == ErrProfileAlreadyExists {
631 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}})
632 return
633 }
634 log.Printf("Error saving profile: %#+v\n", err)
635 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
636 return
637 }
638 logins := []Login{}
639 login := Login{
640 Type: "email",
641 Value: req.Email,
642 Created: profile.Created,
643 LastUsed: profile.Created,
644 ProfileID: profile.ID,
645 Verification: uuid.NewID().String(),
646 }
647 err = context.AddLogin(login)
648 if err != nil {
649 if err == ErrLoginAlreadyExists {
650 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}})
651 return
652 }
653 log.Printf("Error adding login: %#+v\n", err)
654 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
655 return
656 }
657 verification := login.Verification
658 login.Verification = "" // clear verification so it's not exposed
659 logins = append(logins, login)
660 resp := Response{
661 Logins: logins,
662 Profiles: []Profile{profile},
663 }
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)
668 }
670 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
671 errors := []RequestError{}
672 vars := mux.Vars(r)
673 if vars["id"] == "" {
674 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
675 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
676 return
677 }
678 id, err := uuid.Parse(vars["id"])
679 if err != nil {
680 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
681 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
682 return
683 }
684 username, password, ok := r.BasicAuth()
685 if !ok {
686 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
687 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
688 return
689 }
690 profile, err := authenticate(username, password, context)
691 if err != nil {
692 if isAuthError(err) {
693 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
694 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
695 } else {
696 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
697 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
698 }
699 return
700 }
701 if !profile.ID.Equal(id) {
702 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
703 encode(w, r, http.StatusForbidden, Response{Errors: errors})
704 return
705 }
706 var req ProfileChange
707 decoder := json.NewDecoder(r.Body)
708 err = decoder.Decode(&req)
709 if err != nil {
710 log.Printf("Error decoding request: %#+v\n", err)
711 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
712 return
713 }
714 req.Iterations = nil
715 req.Salt = nil
716 req.PassphraseScheme = nil
717 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
718 req.LockedUntil = nil
719 req.LastSeen = 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})
724 return
725 }
726 if len(*req.Passphrase) > MaxPassphraseLength {
727 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"})
728 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
729 return
730 }
731 iterations := context.config.iterations
732 scheme, ok := passphraseSchemes[CurPassphraseScheme]
733 if !ok {
734 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
735 return
736 }
737 curScheme := CurPassphraseScheme
738 req.PassphraseScheme = &curScheme
739 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
740 if err != nil {
741 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
742 return
743 }
744 req.Passphrase = &passphrase
745 req.Salt = &salt
746 req.Iterations = &iterations
747 }
748 if req.PassphraseReset != nil {
749 now := time.Now()
750 req.PassphraseResetCreated = &now
751 }
752 err = req.Validate()
753 if err != nil {
754 var status int
755 var resp Response
756 switch err {
757 case ErrEmptyChange:
758 resp.Profiles = []Profile{profile}
759 status = http.StatusOK
760 default:
761 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
762 resp.Errors = errors
763 status = http.StatusInternalServerError
764 }
765 encode(w, r, status, resp)
766 return
767 }
768 err = context.UpdateProfile(id, req)
769 if err != nil {
770 if err == ErrProfileNotFound {
771 errors = append(errors, RequestError{Slug: RequestErrNotFound})
772 encode(w, r, http.StatusNotFound, Response{Errors: errors})
773 return
774 }
775 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
776 return
777 }
778 profile.ApplyChange(req)
779 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
780 return
781 }
783 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
784 errors := []RequestError{}
785 vars := mux.Vars(r)
786 if vars["id"] == "" {
787 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
788 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
789 return
790 }
791 id, err := uuid.Parse(vars["id"])
792 if err != nil {
793 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
794 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
795 return
796 }
797 username, password, ok := r.BasicAuth()
798 if !ok {
799 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
800 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
801 return
802 }
803 profile, err := authenticate(username, password, context)
804 if err != nil {
805 if isAuthError(err) {
806 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
807 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
808 } else {
809 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
810 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
811 }
812 return
813 }
814 if !profile.ID.Equal(id) {
815 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
816 encode(w, r, http.StatusForbidden, Response{Errors: errors})
817 return
818 }
819 err = context.DeleteProfile(id)
820 if err != nil {
821 if err == ErrProfileNotFound {
822 errors = append(errors, RequestError{Slug: RequestErrNotFound})
823 encode(w, r, http.StatusNotFound, Response{Errors: errors})
824 return
825 }
826 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
827 return
828 }
829 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
830 go cleanUpAfterProfileDeletion(profile.ID, context)
831 }
833 func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
834 var errors []RequestError
835 vars := mux.Vars(r)
836 if vars["login"] == "" {
837 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
838 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
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 // 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}})
856 }
858 func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
859 var errors []RequestError
860 vars := mux.Vars(r)
861 if vars["login"] == "" {
862 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
863 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
864 return
865 }
866 var req LoginChange
867 decoder := json.NewDecoder(r.Body)
868 err := decoder.Decode(&req)
869 if err != nil {
870 log.Printf("Error decoding request: %#+v\n", err)
871 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
872 return
873 }
874 login, err := context.GetLogin(vars["login"])
875 if err != nil {
876 if err == ErrLoginNotFound {
877 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
878 encode(w, r, http.StatusNotFound, Response{Errors: errors})
879 return
880 }
881 log.Printf("Error retrieving login: %#+v\n", err)
882 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
883 return
884 }
885 if req.Verification != nil {
886 err = context.VerifyLogin(vars["login"], *req.Verification)
887 if err != nil {
888 if err == ErrLoginNotFound {
889 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
890 encode(w, r, http.StatusNotFound, Response{Errors: errors})
891 return
892 } else if err == ErrLoginVerificationInvalid {
893 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"})
894 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
895 return
896 }
897 log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err)
898 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
899 return
900 }
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})
907 return
908 }
909 go context.SendModelEvent(login, authEvents.ActionResendVerification)
910 } else {
911 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
912 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
913 return
914 }
915 // clear the Verification code so it's not exposed
916 login.Verification = ""
917 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
918 }