auth
auth/profile.go
The great documentation and exported interface cleanup. Modify all our *Store interfaces to be unexported, as there's no real good reason they need to be exported, especially as they can be implemented without being exported. The interfaces shouldn't matter to 99% of users of the package, so let's not pollute our package API. Further, all methods of the interfaces are now unexported, for pretty much the same reasoning. Add a doc.go file with documentation explaining the choices the package is making and what it provides. Implement documentation on all our exported types and methods and functions, which makes golint happy. The only remaining golint warning is about NewMemstore, which will stay the way it is. The memstore type is useful outside tests for things like standing up a server quickly when we don't care about the storage, and because the type is unexported, we _need_ a New function to create an instance that can be passed to the Context.
| 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@57 | 11 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive. |
| paddy@48 | 12 MinPassphraseLength = 6 |
| paddy@57 | 13 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive. |
| paddy@48 | 14 MaxPassphraseLength = 64 |
| paddy@48 | 15 ) |
| paddy@48 | 16 |
| paddy@38 | 17 var ( |
| paddy@57 | 18 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first. |
| paddy@57 | 19 ErrNoProfileStore = errors.New("no profileStore was specified for the Context") |
| paddy@57 | 20 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with |
| paddy@57 | 21 // the same ID already exists in the profileStore. |
| paddy@57 | 22 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore") |
| paddy@57 | 23 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore. |
| paddy@57 | 24 ErrProfileNotFound = errors.New("profile not found in profileStore") |
| paddy@57 | 25 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same |
| paddy@57 | 26 // Type and Value already exists in the profileStore. |
| paddy@57 | 27 ErrLoginAlreadyExists = errors.New("login already exists in profileStore") |
| paddy@57 | 28 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore. |
| paddy@57 | 29 ErrLoginNotFound = errors.New("login not found in profileStore") |
| paddy@48 | 30 |
| paddy@57 | 31 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a |
| paddy@57 | 32 // Passphrase, and requires one. |
| paddy@57 | 33 ErrMissingPassphrase = errors.New("missing passphrase") |
| paddy@57 | 34 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain |
| paddy@57 | 35 // a PassphraseReset, and requires one. |
| paddy@57 | 36 ErrMissingPassphraseReset = errors.New("missing passphrase reset") |
| paddy@57 | 37 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not |
| paddy@57 | 38 // contain a PassphraseResetCreated, and requires one. |
| paddy@48 | 39 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp") |
| paddy@57 | 40 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase, |
| paddy@57 | 41 // but the Passphrase is shorter than MinPassphraseLength. |
| paddy@57 | 42 ErrPassphraseTooShort = errors.New("passphrase too short") |
| paddy@57 | 43 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase, |
| paddy@57 | 44 // but the Passphrase is longer than MaxPassphraseLength. |
| paddy@57 | 45 ErrPassphraseTooLong = errors.New("passphrase too long") |
| paddy@38 | 46 ) |
| paddy@38 | 47 |
| paddy@57 | 48 // Profile represents a single user of the service, |
| paddy@57 | 49 // including their authentication information, but not |
| paddy@57 | 50 // including their username or email. |
| paddy@27 | 51 type Profile struct { |
| paddy@38 | 52 ID uuid.ID |
| paddy@38 | 53 Name string |
| paddy@38 | 54 Passphrase string |
| paddy@38 | 55 Iterations int64 |
| paddy@38 | 56 Salt string |
| paddy@38 | 57 PassphraseScheme int |
| paddy@38 | 58 Compromised bool |
| paddy@38 | 59 LockedUntil time.Time |
| paddy@38 | 60 PassphraseReset string |
| paddy@38 | 61 PassphraseResetCreated time.Time |
| paddy@38 | 62 Created time.Time |
| paddy@38 | 63 LastSeen time.Time |
| paddy@38 | 64 } |
| paddy@38 | 65 |
| paddy@57 | 66 // ApplyChange applies the properties of the passed ProfileChange |
| paddy@57 | 67 // to the Profile it is called on. |
| paddy@38 | 68 func (p *Profile) ApplyChange(change ProfileChange) { |
| paddy@38 | 69 if change.Name != nil { |
| paddy@38 | 70 p.Name = *change.Name |
| paddy@38 | 71 } |
| paddy@38 | 72 if change.Passphrase != nil { |
| paddy@38 | 73 p.Passphrase = *change.Passphrase |
| paddy@38 | 74 } |
| paddy@38 | 75 if change.Iterations != nil { |
| paddy@38 | 76 p.Iterations = *change.Iterations |
| paddy@38 | 77 } |
| paddy@38 | 78 if change.Salt != nil { |
| paddy@38 | 79 p.Salt = *change.Salt |
| paddy@38 | 80 } |
| paddy@38 | 81 if change.PassphraseScheme != nil { |
| paddy@38 | 82 p.PassphraseScheme = *change.PassphraseScheme |
| paddy@38 | 83 } |
| paddy@38 | 84 if change.Compromised != nil { |
| paddy@38 | 85 p.Compromised = *change.Compromised |
| paddy@38 | 86 } |
| paddy@38 | 87 if change.LockedUntil != nil { |
| paddy@38 | 88 p.LockedUntil = *change.LockedUntil |
| paddy@38 | 89 } |
| paddy@38 | 90 if change.PassphraseReset != nil { |
| paddy@38 | 91 p.PassphraseReset = *change.PassphraseReset |
| paddy@38 | 92 } |
| paddy@38 | 93 if change.PassphraseResetCreated != nil { |
| paddy@38 | 94 p.PassphraseResetCreated = *change.PassphraseResetCreated |
| paddy@38 | 95 } |
| paddy@38 | 96 if change.LastSeen != nil { |
| paddy@38 | 97 p.LastSeen = *change.LastSeen |
| paddy@38 | 98 } |
| paddy@38 | 99 } |
| paddy@38 | 100 |
| paddy@57 | 101 // ApplyBulkChange applies the properties of the passed BulkProfileChange |
| paddy@57 | 102 // to the Profile it is called on. |
| paddy@44 | 103 func (p *Profile) ApplyBulkChange(change BulkProfileChange) { |
| paddy@44 | 104 if change.Compromised != nil { |
| paddy@44 | 105 p.Compromised = *change.Compromised |
| paddy@44 | 106 } |
| paddy@44 | 107 } |
| paddy@44 | 108 |
| paddy@57 | 109 // ProfileChange represents a single atomic change to a Profile's mutable data. |
| paddy@38 | 110 type ProfileChange struct { |
| paddy@38 | 111 Name *string |
| paddy@38 | 112 Passphrase *string |
| paddy@38 | 113 Iterations *int64 |
| paddy@38 | 114 Salt *string |
| paddy@38 | 115 PassphraseScheme *int |
| paddy@38 | 116 Compromised *bool |
| paddy@38 | 117 LockedUntil *time.Time |
| paddy@38 | 118 PassphraseReset *string |
| paddy@38 | 119 PassphraseResetCreated *time.Time |
| paddy@38 | 120 LastSeen *time.Time |
| paddy@38 | 121 } |
| paddy@38 | 122 |
| paddy@57 | 123 // Validate checks the ProfileChange it is called on |
| paddy@57 | 124 // and asserts its internal validity, or lack thereof. |
| paddy@57 | 125 // A descriptive error will be returned in the case of |
| paddy@57 | 126 // an invalid change. |
| paddy@38 | 127 func (c ProfileChange) Validate() error { |
| paddy@48 | 128 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 | 129 return ErrEmptyChange |
| paddy@48 | 130 } |
| paddy@48 | 131 if c.PassphraseScheme != nil && c.Passphrase == nil { |
| paddy@48 | 132 return ErrMissingPassphrase |
| paddy@48 | 133 } |
| paddy@48 | 134 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil { |
| paddy@48 | 135 return ErrMissingPassphraseResetCreated |
| paddy@48 | 136 } |
| paddy@48 | 137 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil { |
| paddy@48 | 138 return ErrMissingPassphraseReset |
| paddy@48 | 139 } |
| paddy@48 | 140 if c.Salt != nil && c.Passphrase == nil { |
| paddy@48 | 141 return ErrMissingPassphrase |
| paddy@48 | 142 } |
| paddy@48 | 143 if c.Iterations != nil && c.Passphrase == nil { |
| paddy@48 | 144 return ErrMissingPassphrase |
| paddy@48 | 145 } |
| paddy@48 | 146 if c.Passphrase != nil && len(*c.Passphrase) < MinPassphraseLength { |
| paddy@48 | 147 return ErrPassphraseTooShort |
| paddy@48 | 148 } |
| paddy@48 | 149 if c.Passphrase != nil && len(*c.Passphrase) > MaxPassphraseLength { |
| paddy@48 | 150 return ErrPassphraseTooLong |
| paddy@48 | 151 } |
| paddy@38 | 152 return nil |
| paddy@27 | 153 } |
| paddy@27 | 154 |
| paddy@57 | 155 // BulkProfileChange represents a single atomic change to many Profiles' mutable data. |
| paddy@57 | 156 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the |
| paddy@57 | 157 // ProfileChange values across many Profiles all at once. |
| paddy@44 | 158 type BulkProfileChange struct { |
| paddy@44 | 159 Compromised *bool |
| paddy@44 | 160 } |
| paddy@44 | 161 |
| paddy@57 | 162 // Validate checks the BulkProfileChange it is called on |
| paddy@57 | 163 // and asserts its internal validity, or lack thereof. |
| paddy@57 | 164 // A descriptive error will be returned in the case of an |
| paddy@57 | 165 // invalid change. |
| paddy@44 | 166 func (b BulkProfileChange) Validate() error { |
| paddy@48 | 167 if b.Compromised == nil { |
| paddy@48 | 168 return ErrEmptyChange |
| paddy@48 | 169 } |
| paddy@44 | 170 return nil |
| paddy@44 | 171 } |
| paddy@44 | 172 |
| paddy@57 | 173 // Login represents a single human-friendly identifier for |
| paddy@57 | 174 // a given Profile that can be used to log into that Profile. |
| paddy@57 | 175 // Each Profile may only have one Login for each Type. |
| paddy@27 | 176 type Login struct { |
| paddy@27 | 177 Type string |
| paddy@27 | 178 Value string |
| paddy@27 | 179 ProfileID uuid.ID |
| paddy@27 | 180 Created time.Time |
| paddy@27 | 181 LastUsed time.Time |
| paddy@27 | 182 } |
| paddy@27 | 183 |
| paddy@57 | 184 type profileStore interface { |
| paddy@57 | 185 getProfileByID(id uuid.ID) (Profile, error) |
| paddy@57 | 186 getProfileByLogin(loginType, value string) (Profile, error) |
| paddy@57 | 187 saveProfile(profile Profile) error |
| paddy@57 | 188 updateProfile(id uuid.ID, change ProfileChange) error |
| paddy@57 | 189 updateProfiles(ids []uuid.ID, change BulkProfileChange) error |
| paddy@57 | 190 deleteProfile(id uuid.ID) error |
| paddy@44 | 191 |
| paddy@57 | 192 addLogin(login Login) error |
| paddy@57 | 193 removeLogin(loginType, value string, profile uuid.ID) error |
| paddy@57 | 194 recordLoginUse(loginType, value string, when time.Time) error |
| paddy@57 | 195 listLogins(profile uuid.ID, num, offset int) ([]Login, error) |
| paddy@38 | 196 } |
| paddy@27 | 197 |
| paddy@57 | 198 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) { |
| paddy@38 | 199 m.profileLock.RLock() |
| paddy@38 | 200 defer m.profileLock.RUnlock() |
| paddy@38 | 201 p, ok := m.profiles[id.String()] |
| paddy@38 | 202 if !ok { |
| paddy@38 | 203 return Profile{}, ErrProfileNotFound |
| paddy@38 | 204 } |
| paddy@38 | 205 return p, nil |
| paddy@27 | 206 } |
| paddy@38 | 207 |
| paddy@57 | 208 func (m *memstore) getProfileByLogin(loginType, value string) (Profile, error) { |
| paddy@44 | 209 m.loginLock.RLock() |
| paddy@44 | 210 defer m.loginLock.RUnlock() |
| paddy@44 | 211 login, ok := m.logins[loginType+":"+value] |
| paddy@44 | 212 if !ok { |
| paddy@44 | 213 return Profile{}, ErrLoginNotFound |
| paddy@44 | 214 } |
| paddy@44 | 215 m.profileLock.RLock() |
| paddy@44 | 216 defer m.profileLock.RUnlock() |
| paddy@44 | 217 profile, ok := m.profiles[login.ProfileID.String()] |
| paddy@44 | 218 if !ok { |
| paddy@44 | 219 return Profile{}, ErrProfileNotFound |
| paddy@44 | 220 } |
| paddy@44 | 221 return profile, nil |
| paddy@38 | 222 } |
| paddy@38 | 223 |
| paddy@57 | 224 func (m *memstore) saveProfile(profile Profile) error { |
| paddy@38 | 225 m.profileLock.Lock() |
| paddy@38 | 226 defer m.profileLock.Unlock() |
| paddy@38 | 227 _, ok := m.profiles[profile.ID.String()] |
| paddy@38 | 228 if ok { |
| paddy@38 | 229 return ErrProfileAlreadyExists |
| paddy@38 | 230 } |
| paddy@38 | 231 m.profiles[profile.ID.String()] = profile |
| paddy@38 | 232 return nil |
| paddy@38 | 233 } |
| paddy@38 | 234 |
| paddy@57 | 235 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error { |
| paddy@38 | 236 m.profileLock.Lock() |
| paddy@38 | 237 defer m.profileLock.Unlock() |
| paddy@38 | 238 p, ok := m.profiles[id.String()] |
| paddy@38 | 239 if !ok { |
| paddy@38 | 240 return ErrProfileNotFound |
| paddy@38 | 241 } |
| paddy@38 | 242 p.ApplyChange(change) |
| paddy@38 | 243 m.profiles[id.String()] = p |
| paddy@38 | 244 return nil |
| paddy@38 | 245 } |
| paddy@38 | 246 |
| paddy@57 | 247 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error { |
| paddy@44 | 248 m.profileLock.Lock() |
| paddy@44 | 249 defer m.profileLock.Unlock() |
| paddy@44 | 250 for id, profile := range m.profiles { |
| paddy@44 | 251 for _, i := range ids { |
| paddy@44 | 252 if id == i.String() { |
| paddy@44 | 253 profile.ApplyBulkChange(change) |
| paddy@44 | 254 m.profiles[id] = profile |
| paddy@44 | 255 break |
| paddy@44 | 256 } |
| paddy@44 | 257 } |
| paddy@44 | 258 } |
| paddy@44 | 259 return nil |
| paddy@44 | 260 } |
| paddy@44 | 261 |
| paddy@57 | 262 func (m *memstore) deleteProfile(id uuid.ID) error { |
| paddy@38 | 263 m.profileLock.Lock() |
| paddy@38 | 264 defer m.profileLock.Unlock() |
| paddy@38 | 265 _, ok := m.profiles[id.String()] |
| paddy@38 | 266 if !ok { |
| paddy@38 | 267 return ErrProfileNotFound |
| paddy@38 | 268 } |
| paddy@38 | 269 delete(m.profiles, id.String()) |
| paddy@38 | 270 return nil |
| paddy@38 | 271 } |
| paddy@40 | 272 |
| paddy@57 | 273 func (m *memstore) addLogin(login Login) error { |
| paddy@44 | 274 m.loginLock.Lock() |
| paddy@44 | 275 defer m.loginLock.Unlock() |
| paddy@44 | 276 _, ok := m.logins[login.Type+":"+login.Value] |
| paddy@44 | 277 if ok { |
| paddy@44 | 278 return ErrLoginAlreadyExists |
| paddy@44 | 279 } |
| paddy@44 | 280 m.logins[login.Type+":"+login.Value] = login |
| paddy@44 | 281 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Type+":"+login.Value) |
| paddy@44 | 282 return nil |
| paddy@44 | 283 } |
| paddy@44 | 284 |
| paddy@57 | 285 func (m *memstore) removeLogin(loginType, value string, profile uuid.ID) error { |
| paddy@44 | 286 m.loginLock.Lock() |
| paddy@44 | 287 defer m.loginLock.Unlock() |
| paddy@44 | 288 l, ok := m.logins[loginType+":"+value] |
| paddy@44 | 289 if !ok { |
| paddy@44 | 290 return ErrLoginNotFound |
| paddy@44 | 291 } |
| paddy@44 | 292 if !l.ProfileID.Equal(profile) { |
| paddy@44 | 293 return ErrLoginNotFound |
| paddy@44 | 294 } |
| paddy@44 | 295 delete(m.logins, loginType+":"+value) |
| paddy@44 | 296 pos := -1 |
| paddy@44 | 297 for p, id := range m.profileLoginLookup[profile.String()] { |
| paddy@44 | 298 if id == loginType+":"+value { |
| paddy@44 | 299 pos = p |
| paddy@44 | 300 break |
| paddy@44 | 301 } |
| paddy@44 | 302 } |
| paddy@44 | 303 if pos >= 0 { |
| paddy@44 | 304 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...) |
| paddy@44 | 305 } |
| paddy@44 | 306 return nil |
| paddy@44 | 307 } |
| paddy@44 | 308 |
| paddy@57 | 309 func (m *memstore) recordLoginUse(loginType, value string, when time.Time) error { |
| paddy@44 | 310 m.loginLock.Lock() |
| paddy@44 | 311 defer m.loginLock.Unlock() |
| paddy@44 | 312 l, ok := m.logins[loginType+":"+value] |
| paddy@44 | 313 if !ok { |
| paddy@44 | 314 return ErrLoginNotFound |
| paddy@44 | 315 } |
| paddy@44 | 316 l.LastUsed = when |
| paddy@44 | 317 m.logins[loginType+":"+value] = l |
| paddy@44 | 318 return nil |
| paddy@44 | 319 } |
| paddy@44 | 320 |
| paddy@57 | 321 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) { |
| paddy@44 | 322 m.loginLock.RLock() |
| paddy@44 | 323 defer m.loginLock.RUnlock() |
| paddy@44 | 324 ids, ok := m.profileLoginLookup[profile.String()] |
| paddy@44 | 325 if !ok { |
| paddy@44 | 326 return []Login{}, nil |
| paddy@44 | 327 } |
| paddy@44 | 328 if len(ids) > num+offset { |
| paddy@44 | 329 ids = ids[offset : num+offset] |
| paddy@44 | 330 } else if len(ids) > offset { |
| paddy@44 | 331 ids = ids[offset:] |
| paddy@44 | 332 } else { |
| paddy@44 | 333 return []Login{}, nil |
| paddy@44 | 334 } |
| paddy@44 | 335 logins := []Login{} |
| paddy@44 | 336 for _, id := range ids { |
| paddy@44 | 337 login, ok := m.logins[id] |
| paddy@44 | 338 if !ok { |
| paddy@44 | 339 continue |
| paddy@44 | 340 } |
| paddy@44 | 341 logins = append(logins, login) |
| paddy@44 | 342 } |
| paddy@44 | 343 return logins, nil |
| paddy@44 | 344 } |