auth

Paddy 2014-11-03 Parent:e45bfa2abc00 Child:42bc3e44f4fe

66:55d5107e8805 Go to Latest

auth/profile.go

Bugfixes and tests for getting grants. Add tests for the grant confirmation part of the request to get a grant code. When the request is denied, the redirect should have an access_denied error. When the request is approved, the redirect should contain a code. Fix numerous bugs in which the redirect URL didn't contain the parameters we thought it would. Basically, anything that still used req.URL.Query().Set() instead of copying the query, modifying it, and setting req.URL.RawQuery. Fix a bug in which the redirectURL wasn't being properly set. Basically, when we moved the redirectURI processing to the top of the file (c29c7df35905 for those who forgot), we didn't update the reference to it lower in the file, where redirectURI was being updated and we expected that to be reflected in the processing. The HTTP handler for getting grant codes is now completely tested except for returning internal errors, which requires a new test harness be built to provoke internal errors on demand. At this point, however, I'd like to continue on implementing endpoints.

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