auth

Paddy 2015-07-15 Parent:5d52b9d83184 Child:7bba108d2d9a

178:0a2c3d677161 Go to Latest

auth/profile.go

Update to use a generic event emitter. Rather can creating a purpose-built event emitter for each and every event we need to emit (I'm looking at you, login verification event) which is _downright silly_, we're now using a generic event publisher that's based on saying "HEY A MODEL UPDATED". This means we need to change all our setup code in authd to use events.NewNSQPublisher or events.NewStdoutPublisher instead of our homegrown solutions. Which also means updating our config to take an events.Publisher instead of our LoginVerificationNotifier (blergh). Our Context also now uses an events.Publisher instead of a LoginVerificationNotifier. Party all around! We also replaced our SendLoginVerification helper method on Context with a SendModelEvent helper method on Context, which is just a light wrapper around events.PublishModelEvent. Of course, all this means we need to update our email_verification listener to listen to the correct channel (based on the model we want updates about) and filter down to a Created action or our new custom action for "the customer wants their verification resent", which I'm OK making a special case and not generic, because c'mon. But we had a subtle change to all our constants, some of which are unofficial constants now. I'm unsure how I feel about this. We also updated our email_verification listener so that we're unmarshalling to a custom loginEvent, which is just an events.Event that overwrites the Data property to be an auth.Login instance. This is to make sure we don't need to wrangle a map[string]interface{}, which is no fun. I'm also OK with special-casing like this, because it's 1) a tiny amount of code, 2) properly utilising composition, and 3) the only way I can think of to cleanly accomplish what I want. I also added a note about GetLogin's deficient handling of logins, namely that it doesn't recognise admins and return Verification codes to them, which would be a useful property for internal tools to take advantage of. Ah well. I updated the Profile and Login implementations so they're now event.Model instances, mainly by just exporting some strings from them through getters that will let us automatically build an Event from them. This lets us use the PublishModelEvent helper. I updated our CreateProfileHandler to properly mangle the login Verification property, and to fire off the ActionCreated events for the new Login and the new Profile. I updated our GetLoginHandler and UpdateLoginHandler to properly mangle the loginVerification property. God that's annoying. :-/ You'll note I didn't start publishing the events.ActionUpdated or events.ActionDeleted events for Profiles or Logins yet, and didn't bother publishing any events for literally any other type. That's because I'm a lazy piece of crap and will end up publishing them when I absolutely have to. Part of that is because if a channel isn't created/being read for a topic, the messages will just stack up in NSQ, and I don't want that. But mostly I'm lazy. Finally, I got to delete the entire profile_verification.go file, because we're no longer special-casing that. Hooray!

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