auth

Paddy 2014-10-26 Parent:73a9f7a6af54 Child:42bc3e44f4fe

57:e45bfa2abc00 Go to Latest

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.

History
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 }