auth
auth/profile.go
Move HTTP tests to http_test.go, rename the GetGrant test. Rename the GetGrant test to something that makes it more obvious, and indicate that we're only testing for successful responses in this function (e.g., responses that should be successfully handled). Another function will deal with failure modes. Move the function to a new http_test.go file. The model files are shouldn't have information about how the models are being represented.
| 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@48 | 10 const ( |
| paddy@48 | 11 MinPassphraseLength = 6 |
| paddy@48 | 12 MaxPassphraseLength = 64 |
| paddy@48 | 13 ) |
| paddy@48 | 14 |
| paddy@38 | 15 var ( |
| paddy@49 | 16 ErrNoProfileStore = errors.New("no ProfileStore was specified for the Context") |
| paddy@38 | 17 ErrProfileAlreadyExists = errors.New("profile already exists in ProfileStore") |
| paddy@38 | 18 ErrProfileNotFound = errors.New("profile not found in ProfileStore") |
| paddy@44 | 19 ErrLoginAlreadyExists = errors.New("login already exists in ProfileStore") |
| paddy@44 | 20 ErrLoginNotFound = errors.New("login not found in ProfileStore") |
| paddy@48 | 21 |
| paddy@48 | 22 ErrMissingPassphrase = errors.New("missing passphrase") |
| paddy@48 | 23 ErrMissingPassphraseReset = errors.New("missing passphrase reset") |
| paddy@48 | 24 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp") |
| paddy@48 | 25 ErrPassphraseTooShort = errors.New("passphrase too short") |
| paddy@48 | 26 ErrPassphraseTooLong = errors.New("passphrase too long") |
| paddy@38 | 27 ) |
| paddy@38 | 28 |
| paddy@27 | 29 type Profile struct { |
| paddy@38 | 30 ID uuid.ID |
| paddy@38 | 31 Name string |
| paddy@38 | 32 Passphrase string |
| paddy@38 | 33 Iterations int64 |
| paddy@38 | 34 Salt string |
| paddy@38 | 35 PassphraseScheme int |
| paddy@38 | 36 Compromised bool |
| paddy@38 | 37 LockedUntil time.Time |
| paddy@38 | 38 PassphraseReset string |
| paddy@38 | 39 PassphraseResetCreated time.Time |
| paddy@38 | 40 Created time.Time |
| paddy@38 | 41 LastSeen time.Time |
| paddy@38 | 42 } |
| paddy@38 | 43 |
| paddy@38 | 44 func (p *Profile) ApplyChange(change ProfileChange) { |
| paddy@38 | 45 if change.Name != nil { |
| paddy@38 | 46 p.Name = *change.Name |
| paddy@38 | 47 } |
| paddy@38 | 48 if change.Passphrase != nil { |
| paddy@38 | 49 p.Passphrase = *change.Passphrase |
| paddy@38 | 50 } |
| paddy@38 | 51 if change.Iterations != nil { |
| paddy@38 | 52 p.Iterations = *change.Iterations |
| paddy@38 | 53 } |
| paddy@38 | 54 if change.Salt != nil { |
| paddy@38 | 55 p.Salt = *change.Salt |
| paddy@38 | 56 } |
| paddy@38 | 57 if change.PassphraseScheme != nil { |
| paddy@38 | 58 p.PassphraseScheme = *change.PassphraseScheme |
| paddy@38 | 59 } |
| paddy@38 | 60 if change.Compromised != nil { |
| paddy@38 | 61 p.Compromised = *change.Compromised |
| paddy@38 | 62 } |
| paddy@38 | 63 if change.LockedUntil != nil { |
| paddy@38 | 64 p.LockedUntil = *change.LockedUntil |
| paddy@38 | 65 } |
| paddy@38 | 66 if change.PassphraseReset != nil { |
| paddy@38 | 67 p.PassphraseReset = *change.PassphraseReset |
| paddy@38 | 68 } |
| paddy@38 | 69 if change.PassphraseResetCreated != nil { |
| paddy@38 | 70 p.PassphraseResetCreated = *change.PassphraseResetCreated |
| paddy@38 | 71 } |
| paddy@38 | 72 if change.LastSeen != nil { |
| paddy@38 | 73 p.LastSeen = *change.LastSeen |
| paddy@38 | 74 } |
| paddy@38 | 75 } |
| paddy@38 | 76 |
| paddy@44 | 77 func (p *Profile) ApplyBulkChange(change BulkProfileChange) { |
| paddy@44 | 78 if change.Compromised != nil { |
| paddy@44 | 79 p.Compromised = *change.Compromised |
| paddy@44 | 80 } |
| paddy@44 | 81 } |
| paddy@44 | 82 |
| paddy@38 | 83 type ProfileChange struct { |
| paddy@38 | 84 Name *string |
| paddy@38 | 85 Passphrase *string |
| paddy@38 | 86 Iterations *int64 |
| paddy@38 | 87 Salt *string |
| paddy@38 | 88 PassphraseScheme *int |
| paddy@38 | 89 Compromised *bool |
| paddy@38 | 90 LockedUntil *time.Time |
| paddy@38 | 91 PassphraseReset *string |
| paddy@38 | 92 PassphraseResetCreated *time.Time |
| paddy@38 | 93 LastSeen *time.Time |
| paddy@38 | 94 } |
| paddy@38 | 95 |
| paddy@38 | 96 func (c ProfileChange) Validate() error { |
| paddy@48 | 97 if 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 { |
| paddy@48 | 98 return ErrEmptyChange |
| paddy@48 | 99 } |
| paddy@48 | 100 if c.PassphraseScheme != nil && c.Passphrase == nil { |
| paddy@48 | 101 return ErrMissingPassphrase |
| paddy@48 | 102 } |
| paddy@48 | 103 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil { |
| paddy@48 | 104 return ErrMissingPassphraseResetCreated |
| paddy@48 | 105 } |
| paddy@48 | 106 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil { |
| paddy@48 | 107 return ErrMissingPassphraseReset |
| paddy@48 | 108 } |
| paddy@48 | 109 if c.Salt != nil && c.Passphrase == nil { |
| paddy@48 | 110 return ErrMissingPassphrase |
| paddy@48 | 111 } |
| paddy@48 | 112 if c.Iterations != nil && c.Passphrase == nil { |
| paddy@48 | 113 return ErrMissingPassphrase |
| paddy@48 | 114 } |
| paddy@48 | 115 if c.Passphrase != nil && len(*c.Passphrase) < MinPassphraseLength { |
| paddy@48 | 116 return ErrPassphraseTooShort |
| paddy@48 | 117 } |
| paddy@48 | 118 if c.Passphrase != nil && len(*c.Passphrase) > MaxPassphraseLength { |
| paddy@48 | 119 return ErrPassphraseTooLong |
| paddy@48 | 120 } |
| paddy@38 | 121 return nil |
| paddy@27 | 122 } |
| paddy@27 | 123 |
| paddy@44 | 124 type BulkProfileChange struct { |
| paddy@44 | 125 Compromised *bool |
| paddy@44 | 126 } |
| paddy@44 | 127 |
| paddy@44 | 128 func (b BulkProfileChange) Validate() error { |
| paddy@48 | 129 if b.Compromised == nil { |
| paddy@48 | 130 return ErrEmptyChange |
| paddy@48 | 131 } |
| paddy@44 | 132 return nil |
| paddy@44 | 133 } |
| paddy@44 | 134 |
| paddy@27 | 135 type Login struct { |
| paddy@27 | 136 Type string |
| paddy@27 | 137 Value string |
| paddy@27 | 138 ProfileID uuid.ID |
| paddy@27 | 139 Created time.Time |
| paddy@27 | 140 LastUsed time.Time |
| paddy@27 | 141 } |
| paddy@27 | 142 |
| paddy@27 | 143 type ProfileStore interface { |
| paddy@27 | 144 GetProfileByID(id uuid.ID) (Profile, error) |
| paddy@44 | 145 GetProfileByLogin(loginType, value string) (Profile, error) |
| paddy@38 | 146 SaveProfile(profile Profile) error |
| paddy@38 | 147 UpdateProfile(id uuid.ID, change ProfileChange) error |
| paddy@44 | 148 UpdateProfiles(ids []uuid.ID, change BulkProfileChange) error |
| paddy@27 | 149 DeleteProfile(id uuid.ID) error |
| paddy@44 | 150 |
| paddy@44 | 151 AddLogin(login Login) error |
| paddy@44 | 152 RemoveLogin(loginType, value string, profile uuid.ID) error |
| paddy@44 | 153 RecordLoginUse(loginType, value string, when time.Time) error |
| paddy@44 | 154 ListLogins(profile uuid.ID, num, offset int) ([]Login, error) |
| paddy@38 | 155 } |
| paddy@27 | 156 |
| paddy@38 | 157 func (m *Memstore) GetProfileByID(id uuid.ID) (Profile, error) { |
| paddy@38 | 158 m.profileLock.RLock() |
| paddy@38 | 159 defer m.profileLock.RUnlock() |
| paddy@38 | 160 p, ok := m.profiles[id.String()] |
| paddy@38 | 161 if !ok { |
| paddy@38 | 162 return Profile{}, ErrProfileNotFound |
| paddy@38 | 163 } |
| paddy@38 | 164 return p, nil |
| paddy@27 | 165 } |
| paddy@38 | 166 |
| paddy@44 | 167 func (m *Memstore) GetProfileByLogin(loginType, value string) (Profile, error) { |
| paddy@44 | 168 m.loginLock.RLock() |
| paddy@44 | 169 defer m.loginLock.RUnlock() |
| paddy@44 | 170 login, ok := m.logins[loginType+":"+value] |
| paddy@44 | 171 if !ok { |
| paddy@44 | 172 return Profile{}, ErrLoginNotFound |
| paddy@44 | 173 } |
| paddy@44 | 174 m.profileLock.RLock() |
| paddy@44 | 175 defer m.profileLock.RUnlock() |
| paddy@44 | 176 profile, ok := m.profiles[login.ProfileID.String()] |
| paddy@44 | 177 if !ok { |
| paddy@44 | 178 return Profile{}, ErrProfileNotFound |
| paddy@44 | 179 } |
| paddy@44 | 180 return profile, nil |
| paddy@38 | 181 } |
| paddy@38 | 182 |
| paddy@38 | 183 func (m *Memstore) SaveProfile(profile Profile) error { |
| paddy@38 | 184 m.profileLock.Lock() |
| paddy@38 | 185 defer m.profileLock.Unlock() |
| paddy@38 | 186 _, ok := m.profiles[profile.ID.String()] |
| paddy@38 | 187 if ok { |
| paddy@38 | 188 return ErrProfileAlreadyExists |
| paddy@38 | 189 } |
| paddy@38 | 190 m.profiles[profile.ID.String()] = profile |
| paddy@38 | 191 return nil |
| paddy@38 | 192 } |
| paddy@38 | 193 |
| paddy@38 | 194 func (m *Memstore) UpdateProfile(id uuid.ID, change ProfileChange) error { |
| paddy@38 | 195 m.profileLock.Lock() |
| paddy@38 | 196 defer m.profileLock.Unlock() |
| paddy@38 | 197 p, ok := m.profiles[id.String()] |
| paddy@38 | 198 if !ok { |
| paddy@38 | 199 return ErrProfileNotFound |
| paddy@38 | 200 } |
| paddy@38 | 201 p.ApplyChange(change) |
| paddy@38 | 202 m.profiles[id.String()] = p |
| paddy@38 | 203 return nil |
| paddy@38 | 204 } |
| paddy@38 | 205 |
| paddy@44 | 206 func (m *Memstore) UpdateProfiles(ids []uuid.ID, change BulkProfileChange) error { |
| paddy@44 | 207 m.profileLock.Lock() |
| paddy@44 | 208 defer m.profileLock.Unlock() |
| paddy@44 | 209 for id, profile := range m.profiles { |
| paddy@44 | 210 for _, i := range ids { |
| paddy@44 | 211 if id == i.String() { |
| paddy@44 | 212 profile.ApplyBulkChange(change) |
| paddy@44 | 213 m.profiles[id] = profile |
| paddy@44 | 214 break |
| paddy@44 | 215 } |
| paddy@44 | 216 } |
| paddy@44 | 217 } |
| paddy@44 | 218 return nil |
| paddy@44 | 219 } |
| paddy@44 | 220 |
| paddy@38 | 221 func (m *Memstore) DeleteProfile(id uuid.ID) error { |
| paddy@38 | 222 m.profileLock.Lock() |
| paddy@38 | 223 defer m.profileLock.Unlock() |
| paddy@38 | 224 _, ok := m.profiles[id.String()] |
| paddy@38 | 225 if !ok { |
| paddy@38 | 226 return ErrProfileNotFound |
| paddy@38 | 227 } |
| paddy@38 | 228 delete(m.profiles, id.String()) |
| paddy@38 | 229 return nil |
| paddy@38 | 230 } |
| paddy@40 | 231 |
| paddy@44 | 232 func (m *Memstore) AddLogin(login Login) error { |
| paddy@44 | 233 m.loginLock.Lock() |
| paddy@44 | 234 defer m.loginLock.Unlock() |
| paddy@44 | 235 _, ok := m.logins[login.Type+":"+login.Value] |
| paddy@44 | 236 if ok { |
| paddy@44 | 237 return ErrLoginAlreadyExists |
| paddy@44 | 238 } |
| paddy@44 | 239 m.logins[login.Type+":"+login.Value] = login |
| paddy@44 | 240 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Type+":"+login.Value) |
| paddy@44 | 241 return nil |
| paddy@44 | 242 } |
| paddy@44 | 243 |
| paddy@44 | 244 func (m *Memstore) RemoveLogin(loginType, value string, profile uuid.ID) error { |
| paddy@44 | 245 m.loginLock.Lock() |
| paddy@44 | 246 defer m.loginLock.Unlock() |
| paddy@44 | 247 l, ok := m.logins[loginType+":"+value] |
| paddy@44 | 248 if !ok { |
| paddy@44 | 249 return ErrLoginNotFound |
| paddy@44 | 250 } |
| paddy@44 | 251 if !l.ProfileID.Equal(profile) { |
| paddy@44 | 252 return ErrLoginNotFound |
| paddy@44 | 253 } |
| paddy@44 | 254 delete(m.logins, loginType+":"+value) |
| paddy@44 | 255 pos := -1 |
| paddy@44 | 256 for p, id := range m.profileLoginLookup[profile.String()] { |
| paddy@44 | 257 if id == loginType+":"+value { |
| paddy@44 | 258 pos = p |
| paddy@44 | 259 break |
| paddy@44 | 260 } |
| paddy@44 | 261 } |
| paddy@44 | 262 if pos >= 0 { |
| paddy@44 | 263 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...) |
| paddy@44 | 264 } |
| paddy@44 | 265 return nil |
| paddy@44 | 266 } |
| paddy@44 | 267 |
| paddy@44 | 268 func (m *Memstore) RecordLoginUse(loginType, value string, when time.Time) error { |
| paddy@44 | 269 m.loginLock.Lock() |
| paddy@44 | 270 defer m.loginLock.Unlock() |
| paddy@44 | 271 l, ok := m.logins[loginType+":"+value] |
| paddy@44 | 272 if !ok { |
| paddy@44 | 273 return ErrLoginNotFound |
| paddy@44 | 274 } |
| paddy@44 | 275 l.LastUsed = when |
| paddy@44 | 276 m.logins[loginType+":"+value] = l |
| paddy@44 | 277 return nil |
| paddy@44 | 278 } |
| paddy@44 | 279 |
| paddy@44 | 280 func (m *Memstore) ListLogins(profile uuid.ID, num, offset int) ([]Login, error) { |
| paddy@44 | 281 m.loginLock.RLock() |
| paddy@44 | 282 defer m.loginLock.RUnlock() |
| paddy@44 | 283 ids, ok := m.profileLoginLookup[profile.String()] |
| paddy@44 | 284 if !ok { |
| paddy@44 | 285 return []Login{}, nil |
| paddy@44 | 286 } |
| paddy@44 | 287 if len(ids) > num+offset { |
| paddy@44 | 288 ids = ids[offset : num+offset] |
| paddy@44 | 289 } else if len(ids) > offset { |
| paddy@44 | 290 ids = ids[offset:] |
| paddy@44 | 291 } else { |
| paddy@44 | 292 return []Login{}, nil |
| paddy@44 | 293 } |
| paddy@44 | 294 logins := []Login{} |
| paddy@44 | 295 for _, id := range ids { |
| paddy@44 | 296 login, ok := m.logins[id] |
| paddy@44 | 297 if !ok { |
| paddy@44 | 298 continue |
| paddy@44 | 299 } |
| paddy@44 | 300 logins = append(logins, login) |
| paddy@44 | 301 } |
| paddy@44 | 302 return logins, nil |
| paddy@44 | 303 } |