auth
auth/profile.go
Add simple tests for logins. Test for login adding, removal, listing, and updating, to ensure they work as intended across all our ProfileStores.
| paddy@27 | 1 package auth |
| paddy@27 | 2 |
| paddy@27 | 3 import ( |
| paddy@38 | 4 "errors" |
| paddy@27 | 5 "time" |
| paddy@27 | 6 |
| paddy@45 | 7 "code.secondbit.org/uuid" |
| paddy@27 | 8 ) |
| paddy@27 | 9 |
| paddy@38 | 10 var ( |
| paddy@38 | 11 ErrProfileAlreadyExists = errors.New("profile already exists in ProfileStore") |
| paddy@38 | 12 ErrProfileNotFound = errors.New("profile not found in ProfileStore") |
| paddy@44 | 13 ErrLoginAlreadyExists = errors.New("login already exists in ProfileStore") |
| paddy@44 | 14 ErrLoginNotFound = errors.New("login not found in ProfileStore") |
| paddy@38 | 15 ) |
| paddy@38 | 16 |
| paddy@27 | 17 type Profile struct { |
| paddy@38 | 18 ID uuid.ID |
| paddy@38 | 19 Name string |
| paddy@38 | 20 Passphrase string |
| paddy@38 | 21 Iterations int64 |
| paddy@38 | 22 Salt string |
| paddy@38 | 23 PassphraseScheme int |
| paddy@38 | 24 Compromised bool |
| paddy@38 | 25 LockedUntil time.Time |
| paddy@38 | 26 PassphraseReset string |
| paddy@38 | 27 PassphraseResetCreated time.Time |
| paddy@38 | 28 Created time.Time |
| paddy@38 | 29 LastSeen time.Time |
| paddy@38 | 30 } |
| paddy@38 | 31 |
| paddy@38 | 32 func (p *Profile) ApplyChange(change ProfileChange) { |
| paddy@38 | 33 if change.Name != nil { |
| paddy@38 | 34 p.Name = *change.Name |
| paddy@38 | 35 } |
| paddy@38 | 36 if change.Passphrase != nil { |
| paddy@38 | 37 p.Passphrase = *change.Passphrase |
| paddy@38 | 38 } |
| paddy@38 | 39 if change.Iterations != nil { |
| paddy@38 | 40 p.Iterations = *change.Iterations |
| paddy@38 | 41 } |
| paddy@38 | 42 if change.Salt != nil { |
| paddy@38 | 43 p.Salt = *change.Salt |
| paddy@38 | 44 } |
| paddy@38 | 45 if change.PassphraseScheme != nil { |
| paddy@38 | 46 p.PassphraseScheme = *change.PassphraseScheme |
| paddy@38 | 47 } |
| paddy@38 | 48 if change.Compromised != nil { |
| paddy@38 | 49 p.Compromised = *change.Compromised |
| paddy@38 | 50 } |
| paddy@38 | 51 if change.LockedUntil != nil { |
| paddy@38 | 52 p.LockedUntil = *change.LockedUntil |
| paddy@38 | 53 } |
| paddy@38 | 54 if change.PassphraseReset != nil { |
| paddy@38 | 55 p.PassphraseReset = *change.PassphraseReset |
| paddy@38 | 56 } |
| paddy@38 | 57 if change.PassphraseResetCreated != nil { |
| paddy@38 | 58 p.PassphraseResetCreated = *change.PassphraseResetCreated |
| paddy@38 | 59 } |
| paddy@38 | 60 if change.LastSeen != nil { |
| paddy@38 | 61 p.LastSeen = *change.LastSeen |
| paddy@38 | 62 } |
| paddy@38 | 63 } |
| paddy@38 | 64 |
| paddy@44 | 65 func (p *Profile) ApplyBulkChange(change BulkProfileChange) { |
| paddy@44 | 66 if change.Compromised != nil { |
| paddy@44 | 67 p.Compromised = *change.Compromised |
| paddy@44 | 68 } |
| paddy@44 | 69 } |
| paddy@44 | 70 |
| paddy@38 | 71 type ProfileChange struct { |
| paddy@38 | 72 Name *string |
| paddy@38 | 73 Passphrase *string |
| paddy@38 | 74 Iterations *int64 |
| paddy@38 | 75 Salt *string |
| paddy@38 | 76 PassphraseScheme *int |
| paddy@38 | 77 Compromised *bool |
| paddy@38 | 78 LockedUntil *time.Time |
| paddy@38 | 79 PassphraseReset *string |
| paddy@38 | 80 PassphraseResetCreated *time.Time |
| paddy@38 | 81 LastSeen *time.Time |
| paddy@38 | 82 } |
| paddy@38 | 83 |
| paddy@38 | 84 func (c ProfileChange) Validate() error { |
| paddy@40 | 85 // TODO: validate profile changes |
| paddy@38 | 86 return nil |
| paddy@27 | 87 } |
| paddy@27 | 88 |
| paddy@44 | 89 type BulkProfileChange struct { |
| paddy@44 | 90 Compromised *bool |
| paddy@44 | 91 } |
| paddy@44 | 92 |
| paddy@44 | 93 func (b BulkProfileChange) Validate() error { |
| paddy@44 | 94 // TODO: validate bulk profile changs |
| paddy@44 | 95 return nil |
| paddy@44 | 96 } |
| paddy@44 | 97 |
| paddy@27 | 98 type Login struct { |
| paddy@27 | 99 Type string |
| paddy@27 | 100 Value string |
| paddy@27 | 101 ProfileID uuid.ID |
| paddy@27 | 102 Created time.Time |
| paddy@27 | 103 LastUsed time.Time |
| paddy@27 | 104 } |
| paddy@27 | 105 |
| paddy@27 | 106 type ProfileStore interface { |
| paddy@27 | 107 GetProfileByID(id uuid.ID) (Profile, error) |
| paddy@44 | 108 GetProfileByLogin(loginType, value string) (Profile, error) |
| paddy@38 | 109 SaveProfile(profile Profile) error |
| paddy@38 | 110 UpdateProfile(id uuid.ID, change ProfileChange) error |
| paddy@44 | 111 UpdateProfiles(ids []uuid.ID, change BulkProfileChange) error |
| paddy@27 | 112 DeleteProfile(id uuid.ID) error |
| paddy@44 | 113 |
| paddy@44 | 114 AddLogin(login Login) error |
| paddy@44 | 115 RemoveLogin(loginType, value string, profile uuid.ID) error |
| paddy@44 | 116 RecordLoginUse(loginType, value string, when time.Time) error |
| paddy@44 | 117 ListLogins(profile uuid.ID, num, offset int) ([]Login, error) |
| paddy@38 | 118 } |
| paddy@27 | 119 |
| paddy@38 | 120 func (m *Memstore) GetProfileByID(id uuid.ID) (Profile, error) { |
| paddy@38 | 121 m.profileLock.RLock() |
| paddy@38 | 122 defer m.profileLock.RUnlock() |
| paddy@38 | 123 p, ok := m.profiles[id.String()] |
| paddy@38 | 124 if !ok { |
| paddy@38 | 125 return Profile{}, ErrProfileNotFound |
| paddy@38 | 126 } |
| paddy@38 | 127 return p, nil |
| paddy@27 | 128 } |
| paddy@38 | 129 |
| paddy@44 | 130 func (m *Memstore) GetProfileByLogin(loginType, value string) (Profile, error) { |
| paddy@44 | 131 m.loginLock.RLock() |
| paddy@44 | 132 defer m.loginLock.RUnlock() |
| paddy@44 | 133 login, ok := m.logins[loginType+":"+value] |
| paddy@44 | 134 if !ok { |
| paddy@44 | 135 return Profile{}, ErrLoginNotFound |
| paddy@44 | 136 } |
| paddy@44 | 137 m.profileLock.RLock() |
| paddy@44 | 138 defer m.profileLock.RUnlock() |
| paddy@44 | 139 profile, ok := m.profiles[login.ProfileID.String()] |
| paddy@44 | 140 if !ok { |
| paddy@44 | 141 return Profile{}, ErrProfileNotFound |
| paddy@44 | 142 } |
| paddy@44 | 143 return profile, nil |
| paddy@38 | 144 } |
| paddy@38 | 145 |
| paddy@38 | 146 func (m *Memstore) SaveProfile(profile Profile) error { |
| paddy@38 | 147 m.profileLock.Lock() |
| paddy@38 | 148 defer m.profileLock.Unlock() |
| paddy@38 | 149 _, ok := m.profiles[profile.ID.String()] |
| paddy@38 | 150 if ok { |
| paddy@38 | 151 return ErrProfileAlreadyExists |
| paddy@38 | 152 } |
| paddy@38 | 153 m.profiles[profile.ID.String()] = profile |
| paddy@38 | 154 return nil |
| paddy@38 | 155 } |
| paddy@38 | 156 |
| paddy@38 | 157 func (m *Memstore) UpdateProfile(id uuid.ID, change ProfileChange) error { |
| paddy@38 | 158 m.profileLock.Lock() |
| paddy@38 | 159 defer m.profileLock.Unlock() |
| paddy@38 | 160 p, ok := m.profiles[id.String()] |
| paddy@38 | 161 if !ok { |
| paddy@38 | 162 return ErrProfileNotFound |
| paddy@38 | 163 } |
| paddy@38 | 164 p.ApplyChange(change) |
| paddy@38 | 165 m.profiles[id.String()] = p |
| paddy@38 | 166 return nil |
| paddy@38 | 167 } |
| paddy@38 | 168 |
| paddy@44 | 169 func (m *Memstore) UpdateProfiles(ids []uuid.ID, change BulkProfileChange) error { |
| paddy@44 | 170 m.profileLock.Lock() |
| paddy@44 | 171 defer m.profileLock.Unlock() |
| paddy@44 | 172 for id, profile := range m.profiles { |
| paddy@44 | 173 for _, i := range ids { |
| paddy@44 | 174 if id == i.String() { |
| paddy@44 | 175 profile.ApplyBulkChange(change) |
| paddy@44 | 176 m.profiles[id] = profile |
| paddy@44 | 177 break |
| paddy@44 | 178 } |
| paddy@44 | 179 } |
| paddy@44 | 180 } |
| paddy@44 | 181 return nil |
| paddy@44 | 182 } |
| paddy@44 | 183 |
| paddy@38 | 184 func (m *Memstore) DeleteProfile(id uuid.ID) error { |
| paddy@38 | 185 m.profileLock.Lock() |
| paddy@38 | 186 defer m.profileLock.Unlock() |
| paddy@38 | 187 _, ok := m.profiles[id.String()] |
| paddy@38 | 188 if !ok { |
| paddy@38 | 189 return ErrProfileNotFound |
| paddy@38 | 190 } |
| paddy@38 | 191 delete(m.profiles, id.String()) |
| paddy@38 | 192 return nil |
| paddy@38 | 193 } |
| paddy@40 | 194 |
| paddy@44 | 195 func (m *Memstore) AddLogin(login Login) error { |
| paddy@44 | 196 m.loginLock.Lock() |
| paddy@44 | 197 defer m.loginLock.Unlock() |
| paddy@44 | 198 _, ok := m.logins[login.Type+":"+login.Value] |
| paddy@44 | 199 if ok { |
| paddy@44 | 200 return ErrLoginAlreadyExists |
| paddy@44 | 201 } |
| paddy@44 | 202 m.logins[login.Type+":"+login.Value] = login |
| paddy@44 | 203 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Type+":"+login.Value) |
| paddy@44 | 204 return nil |
| paddy@44 | 205 } |
| paddy@44 | 206 |
| paddy@44 | 207 func (m *Memstore) RemoveLogin(loginType, value string, profile uuid.ID) error { |
| paddy@44 | 208 m.loginLock.Lock() |
| paddy@44 | 209 defer m.loginLock.Unlock() |
| paddy@44 | 210 l, ok := m.logins[loginType+":"+value] |
| paddy@44 | 211 if !ok { |
| paddy@44 | 212 return ErrLoginNotFound |
| paddy@44 | 213 } |
| paddy@44 | 214 if !l.ProfileID.Equal(profile) { |
| paddy@44 | 215 return ErrLoginNotFound |
| paddy@44 | 216 } |
| paddy@44 | 217 delete(m.logins, loginType+":"+value) |
| paddy@44 | 218 pos := -1 |
| paddy@44 | 219 for p, id := range m.profileLoginLookup[profile.String()] { |
| paddy@44 | 220 if id == loginType+":"+value { |
| paddy@44 | 221 pos = p |
| paddy@44 | 222 break |
| paddy@44 | 223 } |
| paddy@44 | 224 } |
| paddy@44 | 225 if pos >= 0 { |
| paddy@44 | 226 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...) |
| paddy@44 | 227 } |
| paddy@44 | 228 return nil |
| paddy@44 | 229 } |
| paddy@44 | 230 |
| paddy@44 | 231 func (m *Memstore) RecordLoginUse(loginType, value string, when time.Time) error { |
| paddy@44 | 232 m.loginLock.Lock() |
| paddy@44 | 233 defer m.loginLock.Unlock() |
| paddy@44 | 234 l, ok := m.logins[loginType+":"+value] |
| paddy@44 | 235 if !ok { |
| paddy@44 | 236 return ErrLoginNotFound |
| paddy@44 | 237 } |
| paddy@44 | 238 l.LastUsed = when |
| paddy@44 | 239 m.logins[loginType+":"+value] = l |
| paddy@44 | 240 return nil |
| paddy@44 | 241 } |
| paddy@44 | 242 |
| paddy@44 | 243 func (m *Memstore) ListLogins(profile uuid.ID, num, offset int) ([]Login, error) { |
| paddy@44 | 244 m.loginLock.RLock() |
| paddy@44 | 245 defer m.loginLock.RUnlock() |
| paddy@44 | 246 ids, ok := m.profileLoginLookup[profile.String()] |
| paddy@44 | 247 if !ok { |
| paddy@44 | 248 return []Login{}, nil |
| paddy@44 | 249 } |
| paddy@44 | 250 if len(ids) > num+offset { |
| paddy@44 | 251 ids = ids[offset : num+offset] |
| paddy@44 | 252 } else if len(ids) > offset { |
| paddy@44 | 253 ids = ids[offset:] |
| paddy@44 | 254 } else { |
| paddy@44 | 255 return []Login{}, nil |
| paddy@44 | 256 } |
| paddy@44 | 257 logins := []Login{} |
| paddy@44 | 258 for _, id := range ids { |
| paddy@44 | 259 login, ok := m.logins[id] |
| paddy@44 | 260 if !ok { |
| paddy@44 | 261 continue |
| paddy@44 | 262 } |
| paddy@44 | 263 logins = append(logins, login) |
| paddy@44 | 264 } |
| paddy@44 | 265 return logins, nil |
| paddy@44 | 266 } |