Enable terminating sessions through the API.
Add a terminateSession method to the sessionStore that sets the Active property
of the Session to false.
Create a Context.TerminateSession wrapper for the terminateSession method on the
sessionStore.
Add a Sessions property to our response type so we can return a []Session in API
responses.
Use the URL-safe encoding when base64 encoding our session ID and CSRFToken, so
the ID can be passed in the URL and so our encodings are consistent.
Add a TerminateSessionHandler function that will extract a Session ID from the
request URL, authenticate the user, check that the authenticated user owns the
session in question, and terminate the session.
Add implementations for our new terminateSession method for the memstore and
postgres types.
Test both the memstore and postgres implementation of our terminateSession
helper in session_test.go.
9 "code.secondbit.org/uuid.hg"
13 profileChangeName = 1 << iota
14 profileChangePassphrase
15 profileChangeIterations
17 profileChangePassphraseScheme
18 profileChangeCompromised
19 profileChangeLockedUntil
20 profileChangePassphraseReset
21 profileChangePassphraseResetCreated
26 if os.Getenv("PG_TEST_DB") != "" {
27 p, err := NewPostgres(os.Getenv("PG_TEST_DB"))
31 profileStores = append(profileStores, &p)
35 var profileStores = []profileStore{NewMemstore()}
37 func compareProfiles(profile1, profile2 Profile) (success bool, field string, val1, val2 interface{}) {
38 if !profile1.ID.Equal(profile2.ID) {
39 return false, "ID", profile1.ID, profile2.ID
41 if profile1.Name != profile2.Name {
42 return false, "name", profile1.Name, profile2.Name
44 if profile1.Passphrase != profile2.Passphrase {
45 return false, "passphrase", profile1.Passphrase, profile2.Passphrase
47 if profile1.Iterations != profile2.Iterations {
48 return false, "iterations", profile1.Iterations, profile2.Iterations
50 if profile1.Salt != profile2.Salt {
51 return false, "salt", profile1.Salt, profile2.Salt
53 if profile1.PassphraseScheme != profile2.PassphraseScheme {
54 return false, "passphrase scheme", profile1.PassphraseScheme, profile2.PassphraseScheme
56 if profile1.Compromised != profile2.Compromised {
57 return false, "compromised", profile1.Compromised, profile2.Compromised
59 if !profile1.LockedUntil.Equal(profile2.LockedUntil) {
60 return false, "locked until", profile1.LockedUntil, profile2.LockedUntil
62 if profile1.PassphraseReset != profile2.PassphraseReset {
63 return false, "passphrase reset", profile1.PassphraseReset, profile2.PassphraseReset
65 if !profile1.PassphraseResetCreated.Equal(profile2.PassphraseResetCreated) {
66 return false, "passphrase reset created", profile1.PassphraseResetCreated, profile2.PassphraseResetCreated
68 if !profile1.Created.Equal(profile2.Created) {
69 return false, "created", profile1.Created, profile2.Created
71 if !profile1.LastSeen.Equal(profile2.LastSeen) {
72 return false, "last seen", profile1.LastSeen, profile2.LastSeen
74 if profile1.Deleted != profile2.Deleted {
75 return false, "deleted", profile1.Deleted, profile2.Deleted
77 return true, "", nil, nil
80 func compareLogins(login1, login2 Login) (success bool, field string, val1, val2 interface{}) {
81 if login1.Type != login2.Type {
82 return false, "Type", login1.Type, login2.Type
84 if login1.Value != login2.Value {
85 return false, "Value", login1.Value, login2.Value
87 if !login1.ProfileID.Equal(login2.ProfileID) {
88 return false, "ProfileID", login1.ProfileID, login2.ProfileID
90 if !login1.Created.Equal(login2.Created) {
91 return false, "Created", login1.Created, login2.Created
93 if !login1.LastUsed.Equal(login2.LastUsed) {
94 return false, "LastUsed", login1.LastUsed, login2.LastUsed
96 return true, "", nil, nil
99 func TestProfileStoreSuccess(t *testing.T) {
104 Passphrase: "passphrase",
109 LockedUntil: time.Now().Add(time.Hour).Round(time.Millisecond),
110 PassphraseReset: "passphrase reset",
111 PassphraseResetCreated: time.Now().Round(time.Millisecond),
112 Created: time.Now().Round(time.Millisecond),
113 LastSeen: time.Now().Round(time.Millisecond),
115 for _, store := range profileStores {
116 context := Context{profiles: store}
117 err := context.SaveProfile(profile)
119 t.Errorf("Error saving profile to %T: %s", store, err)
121 err = context.SaveProfile(profile)
122 if err != ErrProfileAlreadyExists {
123 t.Errorf("Expected ErrProfileAlreadyExists from %T, got %+v", store, err)
125 retrieved, err := context.GetProfileByID(profile.ID)
127 t.Errorf("Error retrieving profile from %T: %s", store, err)
129 match, field, expectation, result := compareProfiles(profile, retrieved)
131 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
134 err = context.UpdateProfile(profile.ID, ProfileChange{Deleted: &deleted})
136 t.Errorf("Error removing profile from %T: %s", store, err)
138 retrieved, err = context.GetProfileByID(profile.ID)
139 if err != ErrProfileNotFound {
140 t.Errorf("Expected ErrProfileNotFound from %T, got %+v and %+v", store, retrieved, err)
145 func TestProfileUpdates(t *testing.T) {
147 variations := 1 << 10
151 Passphrase: "passphrase",
156 LockedUntil: time.Now().Add(time.Hour).Round(time.Millisecond),
157 PassphraseReset: "passphrase reset",
158 PassphraseResetCreated: time.Now().Round(time.Millisecond),
159 Created: time.Now().Round(time.Millisecond),
160 LastSeen: time.Now().Round(time.Millisecond),
162 for i := 0; i < variations; i++ {
163 var name, passphrase, salt, passphraseReset string
165 var lockedUntil, passphraseResetCreated, lastSeen time.Time
166 var passphraseScheme int
169 profile.ID = uuid.NewID()
170 change := ProfileChange{}
171 expectation := profile
173 if i&profileChangeName != 0 {
174 name = fmt.Sprintf("name-%d", i)
176 expectation.Name = name
178 if i&profileChangePassphrase != 0 {
179 passphrase = fmt.Sprintf("passphrase-%d", i)
180 change.Passphrase = &passphrase
181 expectation.Passphrase = passphrase
183 if i&profileChangeIterations != 0 {
185 change.Iterations = &iterations
186 expectation.Iterations = iterations
188 if i&profileChangeSalt != 0 {
189 salt = fmt.Sprintf("salt-%d", i)
191 expectation.Salt = salt
193 if i&profileChangePassphraseScheme != 0 {
195 change.PassphraseScheme = &passphraseScheme
196 expectation.PassphraseScheme = passphraseScheme
198 if i&profileChangeCompromised != 0 {
199 compromised = i%2 != 0
200 change.Compromised = &compromised
201 expectation.Compromised = compromised
203 if i&profileChangeLockedUntil != 0 {
204 lockedUntil = time.Now().Round(time.Millisecond)
205 change.LockedUntil = &lockedUntil
206 expectation.LockedUntil = lockedUntil
208 if i&profileChangePassphraseReset != 0 {
209 passphraseReset = fmt.Sprintf("passphraseReset-%d", i)
210 change.PassphraseReset = &passphraseReset
211 expectation.PassphraseReset = passphraseReset
213 if i&profileChangePassphraseResetCreated != 0 {
214 passphraseResetCreated = time.Now().Round(time.Millisecond)
215 change.PassphraseResetCreated = &passphraseResetCreated
216 expectation.PassphraseResetCreated = passphraseResetCreated
218 if i&profileChangeLastSeen != 0 {
219 lastSeen = time.Now().Round(time.Millisecond)
220 change.LastSeen = &lastSeen
221 expectation.LastSeen = lastSeen
223 result.ApplyChange(change)
224 match, field, expected, got := compareProfiles(expectation, result)
226 t.Errorf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
228 for _, store := range profileStores {
229 context := Context{profiles: store}
230 err := context.SaveProfile(profile)
232 t.Errorf("Error saving profile in %T: %s", store, err)
234 err = context.UpdateProfile(profile.ID, change)
236 t.Errorf("Error updating profile in %T: %s", store, err)
238 retrieved, err := context.GetProfileByID(profile.ID)
240 t.Errorf("Error getting profile from %T: %s", store, err)
242 match, field, expected, got = compareProfiles(expectation, retrieved)
244 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
250 func TestProfilesUpdates(t *testing.T) {
261 change := BulkProfileChange{
264 for _, store := range profileStores {
265 context := Context{profiles: store}
266 err := context.SaveProfile(profile1)
268 t.Errorf("Error saving profile in %T: %s", store, err)
270 err = context.SaveProfile(profile2)
272 t.Errorf("Error saving profile in %T: %s", store, err)
274 err = context.SaveProfile(profile3)
276 t.Errorf("Error saving profile in %T: %s", store, err)
278 err = context.UpdateProfiles([]uuid.ID{profile1.ID, profile3.ID}, change)
280 t.Errorf("Error updating profile in %T: %s", store, err)
282 profile1.Compromised = truth
283 profile3.Compromised = truth
284 retrieved, err := context.GetProfileByID(profile1.ID)
286 t.Errorf("Error getting profile from %T: %s", store, err)
288 match, field, expected, got := compareProfiles(profile1, retrieved)
290 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
292 retrieved, err = context.GetProfileByID(profile2.ID)
294 t.Errorf("Error getting profile from %T: %s", store, err)
296 match, field, expected, got = compareProfiles(profile2, retrieved)
298 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
300 retrieved, err = context.GetProfileByID(profile3.ID)
302 t.Errorf("Error getting profile from %T: %s", store, err)
304 match, field, expected, got = compareProfiles(profile3, retrieved)
306 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
311 func TestProfileStoreLoginSuccess(t *testing.T) {
316 ProfileID: uuid.NewID(),
317 Created: time.Now().Add(-1 * time.Hour).Round(time.Millisecond),
318 LastUsed: time.Now().Add(-1 * time.Minute).Round(time.Millisecond),
320 for _, store := range profileStores {
321 context := Context{profiles: store}
322 err := context.AddLogin(login)
324 t.Errorf("Error adding login to %T: %s", store, err)
326 err = context.AddLogin(login)
327 if err != ErrLoginAlreadyExists {
328 t.Errorf("Expected ErrLoginAlreadyExists from %T, got %+v", store, err)
330 retrieved, err := context.ListLogins(login.ProfileID, 10, 0)
332 t.Errorf("Error retrieving logins from %T: %s", store, err)
334 if len(retrieved) != 1 {
335 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
337 match, field, expectation, result := compareLogins(login, retrieved[0])
339 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
341 lastUsed := time.Now().Round(time.Millisecond)
342 err = context.RecordLoginUse(login.Value, lastUsed)
344 t.Errorf("Error recording use of login to %T: %s", store, err)
346 login.LastUsed = lastUsed
347 retrieved, err = context.ListLogins(login.ProfileID, 10, 0)
349 t.Errorf("Error retrieving logins from %T: %s", store, err)
351 if len(retrieved) != 1 {
352 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
354 match, field, expectation, result = compareLogins(login, retrieved[0])
356 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
358 err = context.RemoveLogin(login.Value, login.ProfileID)
360 t.Errorf("Error removing login from %T: %s", store, err)
362 retrieved, err = context.ListLogins(login.ProfileID, 10, 0)
363 if len(retrieved) != 0 {
364 t.Errorf("Expected 0 login results from %T, got %d: %+v", store, len(retrieved), retrieved)
366 err = context.RemoveLogin(login.Value, login.ProfileID)
367 if err != ErrLoginNotFound {
368 t.Errorf("Expected ErrLoginNotFound from %T, got %+v", store, err)
373 func TestProfileStoreLoginRetrieval(t *testing.T) {
378 Passphrase: "passphrase",
383 LockedUntil: time.Now().Add(time.Hour).Round(time.Millisecond),
384 PassphraseReset: "passphrase reset",
385 PassphraseResetCreated: time.Now().Round(time.Millisecond),
386 Created: time.Now().Round(time.Millisecond),
387 LastSeen: time.Now().Round(time.Millisecond),
392 ProfileID: profile.ID,
393 Created: time.Now().Add(-1 * time.Hour).Round(time.Millisecond),
394 LastUsed: time.Now().Add(-1 * time.Minute).Round(time.Millisecond),
396 for _, store := range profileStores {
397 context := Context{profiles: store}
398 err := context.SaveProfile(profile)
400 t.Errorf("Error saving profile in %T: %s", store, err)
402 err = context.AddLogin(login)
404 t.Errorf("Error storing login in %T: %s", store, err)
406 retrieved, err := context.GetProfileByLogin(login.Value)
408 t.Errorf("Error retrieving profile by login from %T: %s", store, err)
410 match, field, expectation, result := compareProfiles(profile, retrieved)
412 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
417 func TestProfileChangeValidation(t *testing.T) {
419 passphraseScheme := 1
420 passphraseReset := "reset"
424 enteredName := "Paddy"
425 okPassphrase := "this is a decent passphrase"
427 lockedUntil := time.Now().Round(time.Millisecond)
428 resetCreated := time.Now().Round(time.Millisecond)
429 lastSeen := time.Now().Round(time.Millisecond)
430 changes := map[*ProfileChange]error{
431 &ProfileChange{}: ErrEmptyChange,
432 &ProfileChange{PassphraseScheme: &passphraseScheme}: ErrMissingPassphrase,
433 &ProfileChange{PassphraseScheme: &passphraseScheme, Passphrase: &okPassphrase}: nil,
434 &ProfileChange{PassphraseReset: &passphraseReset}: ErrMissingPassphraseResetCreated,
435 &ProfileChange{PassphraseReset: &passphraseReset, PassphraseResetCreated: &resetCreated}: nil,
436 &ProfileChange{Salt: &salt}: ErrMissingPassphrase,
437 &ProfileChange{Salt: &salt, Passphrase: &okPassphrase}: nil,
438 &ProfileChange{Iterations: &iterations}: ErrMissingPassphrase,
439 &ProfileChange{Iterations: &iterations, Passphrase: &okPassphrase}: nil,
440 &ProfileChange{Passphrase: &okPassphrase}: nil,
441 &ProfileChange{Name: &emptyName}: nil,
442 &ProfileChange{Name: &enteredName}: nil,
443 &ProfileChange{Compromised: &compromised}: nil,
444 &ProfileChange{LockedUntil: &lockedUntil}: nil,
445 &ProfileChange{LastSeen: &lastSeen}: nil,
446 &ProfileChange{PassphraseResetCreated: &resetCreated}: ErrMissingPassphraseReset,
448 for change, expectedErr := range changes {
449 if err := change.Validate(); err != expectedErr {
450 t.Errorf("Expected %+v to give an error of %v, gave %v", change, expectedErr, err)
455 func TestBulkProfileChangeValidation(t *testing.T) {
458 changes := map[*BulkProfileChange]error{
459 &BulkProfileChange{}: ErrEmptyChange,
460 &BulkProfileChange{Compromised: &compromised}: nil,
462 for change, expectedErr := range changes {
463 if err := change.Validate(); err != expectedErr {
464 t.Errorf("Expected %+v to give an error of %v, gave %v", change, expectedErr, err)
469 // BUG(paddy): We need to test the validateNewProfileRequest helper.
470 // BUG(paddy): We need to test the CreateProfileHandler.
471 // BUG(paddy): We need to test that deleting works as we expect.