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