auth

Paddy 2015-06-29 Parent:b0d1b3e39fc8 Child:5d52b9d83184

176:fc68085eb40d Go to Latest

auth/profile.go

Add kubernetes definitions. Define a replication controller that will spin up authd servers (using Ducky right now--other instances should rename the ducky parts appropriately). Also, my understanding of which labels go where may be shaky, which is probably evidenced by the fact that all of these things share the same lables. _Whatever_. It also hooks the generated pods up to the JWT secret volume, so they can properly read the JWT secret. Also, created a LoadBalancer Service that will route traffic to the pods created by the Replication Controller.

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