auth

Paddy 2015-05-12 Parent:c45b946abe78 Child:581c60f8dd23

167:0ff23f3a4ede Go to Latest

auth/profile.go

Implement an endpoint for token information. Implement an endpoint that allows us to look up information on a token. We strip the refresh token before the response is sent to avoid leaking the response token.

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/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")
43 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
44 // Passphrase, and requires one.
45 ErrMissingPassphrase = errors.New("missing passphrase")
46 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
47 // a PassphraseReset, and requires one.
48 ErrMissingPassphraseReset = errors.New("missing passphrase reset")
49 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
50 // contain a PassphraseResetCreated, and requires one.
51 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
52 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
53 // but the Passphrase is shorter than MinPassphraseLength.
54 ErrPassphraseTooShort = errors.New("passphrase too short")
55 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
56 // but the Passphrase is longer than MaxPassphraseLength.
57 ErrPassphraseTooLong = errors.New("passphrase too long")
59 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected
60 // of being compromised.
61 ErrProfileCompromised = errors.New("profile compromised")
62 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain
63 // duration, to prevent brute force attacks.
64 ErrProfileLocked = errors.New("profile locked")
65 )
67 // Profile represents a single user of the service,
68 // including their authentication information.
69 type Profile struct {
70 ID uuid.ID `json:"id,omitempty"`
71 Name string `json:"name,omitempty"`
72 Passphrase string `json:"-"`
73 Iterations int `json:"-"`
74 Salt string `json:"-"`
75 PassphraseScheme int `json:"-"`
76 Compromised bool `json:"-"`
77 LockedUntil time.Time `json:"-"`
78 PassphraseReset string `json:"-"`
79 PassphraseResetCreated time.Time `json:"-"`
80 Created time.Time `json:"created,omitempty"`
81 LastSeen time.Time `json:"last_seen,omitempty"`
82 }
84 // ApplyChange applies the properties of the passed ProfileChange
85 // to the Profile it is called on.
86 func (p *Profile) ApplyChange(change ProfileChange) {
87 if change.Name != nil {
88 p.Name = *change.Name
89 }
90 if change.Passphrase != nil {
91 p.Passphrase = *change.Passphrase
92 }
93 if change.Iterations != nil {
94 p.Iterations = *change.Iterations
95 }
96 if change.Salt != nil {
97 p.Salt = *change.Salt
98 }
99 if change.PassphraseScheme != nil {
100 p.PassphraseScheme = *change.PassphraseScheme
101 }
102 if change.Compromised != nil {
103 p.Compromised = *change.Compromised
104 }
105 if change.LockedUntil != nil {
106 p.LockedUntil = *change.LockedUntil
107 }
108 if change.PassphraseReset != nil {
109 p.PassphraseReset = *change.PassphraseReset
110 }
111 if change.PassphraseResetCreated != nil {
112 p.PassphraseResetCreated = *change.PassphraseResetCreated
113 }
114 if change.LastSeen != nil {
115 p.LastSeen = *change.LastSeen
116 }
117 }
119 // ApplyBulkChange applies the properties of the passed BulkProfileChange
120 // to the Profile it is called on.
121 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
122 if change.Compromised != nil {
123 p.Compromised = *change.Compromised
124 }
125 }
127 // ProfileChange represents a single atomic change to a Profile's mutable data.
128 type ProfileChange struct {
129 Name *string
130 Passphrase *string
131 Iterations *int
132 Salt *string
133 PassphraseScheme *int
134 Compromised *bool
135 LockedUntil *time.Time
136 PassphraseReset *string
137 PassphraseResetCreated *time.Time
138 LastSeen *time.Time
139 }
141 func (c ProfileChange) Empty() bool {
142 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)
143 }
145 // Validate checks the ProfileChange it is called on
146 // and asserts its internal validity, or lack thereof.
147 // A descriptive error will be returned in the case of
148 // an invalid change.
149 func (c ProfileChange) Validate() error {
150 if c.Empty() {
151 return ErrEmptyChange
152 }
153 if c.PassphraseScheme != nil && c.Passphrase == nil {
154 return ErrMissingPassphrase
155 }
156 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
157 return ErrMissingPassphraseResetCreated
158 }
159 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
160 return ErrMissingPassphraseReset
161 }
162 if c.Salt != nil && c.Passphrase == nil {
163 return ErrMissingPassphrase
164 }
165 if c.Iterations != nil && c.Passphrase == nil {
166 return ErrMissingPassphrase
167 }
168 return nil
169 }
171 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
172 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
173 // ProfileChange values across many Profiles all at once.
174 type BulkProfileChange struct {
175 Compromised *bool
176 }
178 func (b BulkProfileChange) Empty() bool {
179 return b.Compromised == nil
180 }
182 // Validate checks the BulkProfileChange it is called on
183 // and asserts its internal validity, or lack thereof.
184 // A descriptive error will be returned in the case of an
185 // invalid change.
186 func (b BulkProfileChange) Validate() error {
187 if b.Empty() {
188 return ErrEmptyChange
189 }
190 return nil
191 }
193 // Login represents a single human-friendly identifier for
194 // a given Profile that can be used to log into that Profile.
195 // Each Profile may only have one Login for each Type.
196 type Login struct {
197 Type string `json:"type,omitempty"`
198 Value string `json:"value,omitempty"`
199 ProfileID uuid.ID `json:"profile_id,omitempty"`
200 Created time.Time `json:"created,omitempty"`
201 LastUsed time.Time `json:"last_used,omitempty"`
202 }
204 type newProfileRequest struct {
205 Email string `json:"email"`
206 Passphrase string `json:"passphrase"`
207 Name string `json:"name"`
208 }
210 func validateNewProfileRequest(req *newProfileRequest) []requestError {
211 errors := []requestError{}
212 req.Name = strings.TrimSpace(req.Name)
213 req.Email = strings.TrimSpace(req.Email)
214 if len(req.Passphrase) < MinPassphraseLength {
215 errors = append(errors, requestError{
216 Slug: requestErrInsufficient,
217 Field: "/passphrase",
218 })
219 }
220 if len(req.Passphrase) > MaxPassphraseLength {
221 errors = append(errors, requestError{
222 Slug: requestErrOverflow,
223 Field: "/passphrase",
224 })
225 }
226 if len(req.Name) > MaxNameLength {
227 errors = append(errors, requestError{
228 Slug: requestErrOverflow,
229 Field: "/name",
230 })
231 }
232 if req.Email == "" {
233 errors = append(errors, requestError{
234 Slug: requestErrMissing,
235 Field: "/email",
236 })
237 }
238 if len(req.Email) > MaxEmailLength {
239 errors = append(errors, requestError{
240 Slug: requestErrOverflow,
241 Field: "/email",
242 })
243 }
244 re := regexp.MustCompile(".+@.+\\..+")
245 if !re.Match([]byte(req.Email)) {
246 errors = append(errors, requestError{
247 Slug: requestErrInvalidFormat,
248 Field: "/email",
249 })
250 }
251 return errors
252 }
254 type profileStore interface {
255 getProfileByID(id uuid.ID) (Profile, error)
256 getProfileByLogin(value string) (Profile, error)
257 saveProfile(profile Profile) error
258 updateProfile(id uuid.ID, change ProfileChange) error
259 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
260 deleteProfile(id uuid.ID) error
262 addLogin(login Login) error
263 removeLogin(value string, profile uuid.ID) error
264 removeLoginsByProfile(profile uuid.ID) error
265 recordLoginUse(value string, when time.Time) error
266 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
267 }
269 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
270 m.profileLock.RLock()
271 defer m.profileLock.RUnlock()
272 p, ok := m.profiles[id.String()]
273 if !ok {
274 return Profile{}, ErrProfileNotFound
275 }
276 return p, nil
277 }
279 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
280 m.loginLock.RLock()
281 defer m.loginLock.RUnlock()
282 login, ok := m.logins[value]
283 if !ok {
284 return Profile{}, ErrLoginNotFound
285 }
286 m.profileLock.RLock()
287 defer m.profileLock.RUnlock()
288 profile, ok := m.profiles[login.ProfileID.String()]
289 if !ok {
290 return Profile{}, ErrProfileNotFound
291 }
292 return profile, nil
293 }
295 func (m *memstore) saveProfile(profile Profile) error {
296 m.profileLock.Lock()
297 defer m.profileLock.Unlock()
298 _, ok := m.profiles[profile.ID.String()]
299 if ok {
300 return ErrProfileAlreadyExists
301 }
302 m.profiles[profile.ID.String()] = profile
303 return nil
304 }
306 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
307 m.profileLock.Lock()
308 defer m.profileLock.Unlock()
309 p, ok := m.profiles[id.String()]
310 if !ok {
311 return ErrProfileNotFound
312 }
313 p.ApplyChange(change)
314 m.profiles[id.String()] = p
315 return nil
316 }
318 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
319 m.profileLock.Lock()
320 defer m.profileLock.Unlock()
321 for id, profile := range m.profiles {
322 for _, i := range ids {
323 if id == i.String() {
324 profile.ApplyBulkChange(change)
325 m.profiles[id] = profile
326 break
327 }
328 }
329 }
330 return nil
331 }
333 func (m *memstore) deleteProfile(id uuid.ID) error {
334 m.profileLock.Lock()
335 defer m.profileLock.Unlock()
336 if _, ok := m.profiles[id.String()]; !ok {
337 return ErrProfileNotFound
338 }
339 delete(m.profiles, id.String())
340 return nil
341 }
343 func (m *memstore) addLogin(login Login) error {
344 m.loginLock.Lock()
345 defer m.loginLock.Unlock()
346 _, ok := m.logins[login.Value]
347 if ok {
348 return ErrLoginAlreadyExists
349 }
350 m.logins[login.Value] = login
351 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
352 return nil
353 }
355 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
356 m.loginLock.Lock()
357 defer m.loginLock.Unlock()
358 l, ok := m.logins[value]
359 if !ok {
360 return ErrLoginNotFound
361 }
362 if !l.ProfileID.Equal(profile) {
363 return ErrLoginNotFound
364 }
365 delete(m.logins, value)
366 pos := -1
367 for p, id := range m.profileLoginLookup[profile.String()] {
368 if id == value {
369 pos = p
370 break
371 }
372 }
373 if pos >= 0 {
374 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
375 }
376 return nil
377 }
379 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
380 m.loginLock.Lock()
381 defer m.loginLock.Unlock()
382 logins, ok := m.profileLoginLookup[profile.String()]
383 if !ok {
384 return ErrProfileNotFound
385 }
386 delete(m.profileLoginLookup, profile.String())
387 for _, login := range logins {
388 delete(m.logins, login)
389 }
390 return nil
391 }
393 func (m *memstore) recordLoginUse(value string, when time.Time) error {
394 m.loginLock.Lock()
395 defer m.loginLock.Unlock()
396 l, ok := m.logins[value]
397 if !ok {
398 return ErrLoginNotFound
399 }
400 l.LastUsed = when
401 m.logins[value] = l
402 return nil
403 }
405 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
406 m.loginLock.RLock()
407 defer m.loginLock.RUnlock()
408 ids, ok := m.profileLoginLookup[profile.String()]
409 if !ok {
410 return []Login{}, nil
411 }
412 if len(ids) > num+offset {
413 ids = ids[offset : num+offset]
414 } else if len(ids) > offset {
415 ids = ids[offset:]
416 } else {
417 return []Login{}, nil
418 }
419 logins := []Login{}
420 for _, id := range ids {
421 login, ok := m.logins[id]
422 if !ok {
423 continue
424 }
425 logins = append(logins, login)
426 }
427 return logins, nil
428 }
430 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
431 err := context.RemoveLoginsByProfile(profile)
432 if err != nil {
433 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
434 }
435 err = context.TerminateSessionsByProfile(profile)
436 if err != nil {
437 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
438 }
439 err = context.RevokeTokensByProfileID(profile)
440 if err != nil {
441 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
442 }
443 err = context.DeleteAuthorizationCodesByProfileID(profile)
444 if err != nil {
445 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
446 }
447 clients, err := context.ListClientsByOwner(profile, -1, 0)
448 if err != nil {
449 log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
450 }
451 err = context.DeleteClientsByOwner(profile)
452 if err != nil {
453 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
454 }
455 for _, client := range clients {
456 cleanUpAfterClientDeletion(client.ID, context)
457 }
458 }
460 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
461 func RegisterProfileHandlers(r *mux.Router, context Context) {
462 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS")
463 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS")
464 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS")
465 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS")
466 // TODO: r.Handle("/profiles/{id}/tokens", wrap(context, ListTokensHandler)).Methods("GET", "OPTIONS")
467 // BUG(paddy): We need to implement a handler that will add a login to a profile.
468 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login?
469 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
470 }
472 // GetProfileHandler is an HTTP handler for retrieving a profile.
473 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
474 errors := []requestError{}
475 authz := r.Header.Get("Authorization")
476 if !strings.HasPrefix(authz, "Bearer ") {
477 errors = append(errors, requestError{Slug: requestErrAccessDenied})
478 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
479 return
480 }
481 authz = strings.TrimPrefix(authz, "Bearer ")
482 vars := mux.Vars(r)
483 if vars["id"] == "" {
484 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
485 encode(w, r, http.StatusBadRequest, response{Errors: errors})
486 return
487 }
488 id, err := uuid.Parse(vars["id"])
489 if err != nil {
490 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
491 encode(w, r, http.StatusBadRequest, response{Errors: errors})
492 return
493 }
494 token, err := context.GetToken(authz, false)
495 if err != nil || token.Revoked {
496 if err == ErrTokenNotFound || token.Revoked {
497 errors = append(errors, requestError{Slug: requestErrAccessDenied})
498 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
499 return
500 } else {
501 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
502 return
503 }
504 }
505 if !id.Equal(token.ProfileID) {
506 errors = append(errors, requestError{Slug: requestErrAccessDenied})
507 encode(w, r, http.StatusForbidden, response{Errors: errors})
508 return
509 }
510 profile, err := context.GetProfileByID(id)
511 if err != nil {
512 if err == ErrProfileNotFound {
513 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
514 encode(w, r, http.StatusNotFound, response{Errors: errors})
515 return
516 }
517 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
518 return
519 }
520 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
521 return
522 }
524 // CreateProfileHandler is an HTTP handler for registering new profiles.
525 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
526 scheme, ok := passphraseSchemes[CurPassphraseScheme]
527 if !ok {
528 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
529 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
530 return
531 }
532 var req newProfileRequest
533 errors := []requestError{}
534 decoder := json.NewDecoder(r.Body)
535 err := decoder.Decode(&req)
536 if err != nil {
537 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
538 return
539 }
540 errors = append(errors, validateNewProfileRequest(&req)...)
541 if len(errors) > 0 {
542 encode(w, r, http.StatusBadRequest, response{Errors: errors})
543 return
544 }
545 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
546 if err != nil {
547 log.Printf("Error creating encoded passphrase: %#+v\n", err)
548 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
549 return
550 }
551 profile := Profile{
552 ID: uuid.NewID(),
553 Name: req.Name,
554 Passphrase: string(passphrase),
555 Iterations: context.config.iterations,
556 Salt: string(salt),
557 PassphraseScheme: CurPassphraseScheme,
558 Created: time.Now(),
559 LastSeen: time.Now(),
560 }
561 err = context.SaveProfile(profile)
562 if err != nil {
563 if err == ErrProfileAlreadyExists {
564 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}})
565 return
566 }
567 log.Printf("Error saving profile: %#+v\n", err)
568 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
569 return
570 }
571 logins := []Login{}
572 login := Login{
573 Type: "email",
574 Value: req.Email,
575 Created: profile.Created,
576 LastUsed: profile.Created,
577 ProfileID: profile.ID,
578 }
579 err = context.AddLogin(login)
580 if err != nil {
581 if err == ErrLoginAlreadyExists {
582 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}})
583 return
584 }
585 log.Printf("Error adding login: %#+v\n", err)
586 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
587 return
588 }
589 logins = append(logins, login)
590 resp := response{
591 Logins: logins,
592 Profiles: []Profile{profile},
593 }
594 encode(w, r, http.StatusCreated, resp)
595 // TODO(paddy): should we kick off the email validation flow?
596 }
598 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
599 errors := []requestError{}
600 vars := mux.Vars(r)
601 if vars["id"] == "" {
602 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
603 encode(w, r, http.StatusBadRequest, response{Errors: errors})
604 return
605 }
606 id, err := uuid.Parse(vars["id"])
607 if err != nil {
608 errors = append(errors, requestError{Slug: requestErrAccessDenied})
609 encode(w, r, http.StatusBadRequest, response{Errors: errors})
610 return
611 }
612 username, password, ok := r.BasicAuth()
613 if !ok {
614 errors = append(errors, requestError{Slug: requestErrAccessDenied})
615 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
616 return
617 }
618 profile, err := authenticate(username, password, context)
619 if err != nil {
620 if isAuthError(err) {
621 errors = append(errors, requestError{Slug: requestErrAccessDenied})
622 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
623 } else {
624 errors = append(errors, requestError{Slug: requestErrActOfGod})
625 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
626 }
627 return
628 }
629 if !profile.ID.Equal(id) {
630 errors = append(errors, requestError{Slug: requestErrAccessDenied})
631 encode(w, r, http.StatusForbidden, response{Errors: errors})
632 return
633 }
634 var req ProfileChange
635 decoder := json.NewDecoder(r.Body)
636 err = decoder.Decode(&req)
637 if err != nil {
638 log.Printf("Error decoding request: %#+v\n", err)
639 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
640 return
641 }
642 req.Iterations = nil
643 req.Salt = nil
644 req.PassphraseScheme = nil
645 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
646 req.LockedUntil = nil
647 req.LastSeen = nil
648 if req.Passphrase != nil {
649 if len(*req.Passphrase) < MinPassphraseLength {
650 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/passphrase"})
651 encode(w, r, http.StatusBadRequest, response{Errors: errors})
652 return
653 }
654 if len(*req.Passphrase) > MaxPassphraseLength {
655 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/passphrase"})
656 encode(w, r, http.StatusBadRequest, response{Errors: errors})
657 return
658 }
659 iterations := context.config.iterations
660 scheme, ok := passphraseSchemes[CurPassphraseScheme]
661 if !ok {
662 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
663 return
664 }
665 curScheme := CurPassphraseScheme
666 req.PassphraseScheme = &curScheme
667 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
668 if err != nil {
669 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
670 return
671 }
672 req.Passphrase = &passphrase
673 req.Salt = &salt
674 req.Iterations = &iterations
675 }
676 if req.PassphraseReset != nil {
677 now := time.Now()
678 req.PassphraseResetCreated = &now
679 }
680 err = req.Validate()
681 if err != nil {
682 var status int
683 var resp response
684 switch err {
685 case ErrEmptyChange:
686 resp.Profiles = []Profile{profile}
687 status = http.StatusOK
688 default:
689 errors = append(errors, requestError{Slug: requestErrActOfGod})
690 resp.Errors = errors
691 status = http.StatusInternalServerError
692 }
693 encode(w, r, status, resp)
694 return
695 }
696 err = context.UpdateProfile(id, req)
697 if err != nil {
698 if err == ErrProfileNotFound {
699 errors = append(errors, requestError{Slug: requestErrNotFound})
700 encode(w, r, http.StatusNotFound, response{Errors: errors})
701 return
702 }
703 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
704 return
705 }
706 profile.ApplyChange(req)
707 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
708 return
709 }
711 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
712 errors := []requestError{}
713 vars := mux.Vars(r)
714 if vars["id"] == "" {
715 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
716 encode(w, r, http.StatusBadRequest, response{Errors: errors})
717 return
718 }
719 id, err := uuid.Parse(vars["id"])
720 if err != nil {
721 errors = append(errors, requestError{Slug: requestErrAccessDenied})
722 encode(w, r, http.StatusBadRequest, response{Errors: errors})
723 return
724 }
725 username, password, ok := r.BasicAuth()
726 if !ok {
727 errors = append(errors, requestError{Slug: requestErrAccessDenied})
728 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
729 return
730 }
731 profile, err := authenticate(username, password, context)
732 if err != nil {
733 if isAuthError(err) {
734 errors = append(errors, requestError{Slug: requestErrAccessDenied})
735 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
736 } else {
737 errors = append(errors, requestError{Slug: requestErrActOfGod})
738 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
739 }
740 return
741 }
742 if !profile.ID.Equal(id) {
743 errors = append(errors, requestError{Slug: requestErrAccessDenied})
744 encode(w, r, http.StatusForbidden, response{Errors: errors})
745 return
746 }
747 err = context.DeleteProfile(id)
748 if err != nil {
749 if err == ErrProfileNotFound {
750 errors = append(errors, requestError{Slug: requestErrNotFound})
751 encode(w, r, http.StatusNotFound, response{Errors: errors})
752 return
753 }
754 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
755 return
756 }
757 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
758 go cleanUpAfterProfileDeletion(profile.ID, context)
759 }