auth

Paddy 2014-12-14 Parent:42bc3e44f4fe Child:5bccbed6631b

96:e57a57a944c4 Go to Latest

auth/profile.go

Introduce Config and NewContext. Config is an exported struct that configures the program. It holds all the values that a Context needs. NewContext takes a Config object and builds a Context out of it.

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