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.
7 "code.secondbit.org/uuid"
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
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")
48 // Profile represents a single user of the service,
49 // including their authentication information, but not
50 // including their username or email.
60 PassphraseReset string
61 PassphraseResetCreated time.Time
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 {
72 if change.Passphrase != nil {
73 p.Passphrase = *change.Passphrase
75 if change.Iterations != nil {
76 p.Iterations = *change.Iterations
78 if change.Salt != nil {
81 if change.PassphraseScheme != nil {
82 p.PassphraseScheme = *change.PassphraseScheme
84 if change.Compromised != nil {
85 p.Compromised = *change.Compromised
87 if change.LockedUntil != nil {
88 p.LockedUntil = *change.LockedUntil
90 if change.PassphraseReset != nil {
91 p.PassphraseReset = *change.PassphraseReset
93 if change.PassphraseResetCreated != nil {
94 p.PassphraseResetCreated = *change.PassphraseResetCreated
96 if change.LastSeen != nil {
97 p.LastSeen = *change.LastSeen
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
109 // ProfileChange represents a single atomic change to a Profile's mutable data.
110 type ProfileChange struct {
115 PassphraseScheme *int
117 LockedUntil *time.Time
118 PassphraseReset *string
119 PassphraseResetCreated *time.Time
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
131 if c.PassphraseScheme != nil && c.Passphrase == nil {
132 return ErrMissingPassphrase
134 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
135 return ErrMissingPassphraseResetCreated
137 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
138 return ErrMissingPassphraseReset
140 if c.Salt != nil && c.Passphrase == nil {
141 return ErrMissingPassphrase
143 if c.Iterations != nil && c.Passphrase == nil {
144 return ErrMissingPassphrase
146 if c.Passphrase != nil && len(*c.Passphrase) < MinPassphraseLength {
147 return ErrPassphraseTooShort
149 if c.Passphrase != nil && len(*c.Passphrase) > MaxPassphraseLength {
150 return ErrPassphraseTooLong
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 {
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
166 func (b BulkProfileChange) Validate() error {
167 if b.Compromised == nil {
168 return ErrEmptyChange
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.
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)
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()]
203 return Profile{}, ErrProfileNotFound
208 func (m *memstore) getProfileByLogin(loginType, value string) (Profile, error) {
210 defer m.loginLock.RUnlock()
211 login, ok := m.logins[loginType+":"+value]
213 return Profile{}, ErrLoginNotFound
215 m.profileLock.RLock()
216 defer m.profileLock.RUnlock()
217 profile, ok := m.profiles[login.ProfileID.String()]
219 return Profile{}, ErrProfileNotFound
224 func (m *memstore) saveProfile(profile Profile) error {
226 defer m.profileLock.Unlock()
227 _, ok := m.profiles[profile.ID.String()]
229 return ErrProfileAlreadyExists
231 m.profiles[profile.ID.String()] = profile
235 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
237 defer m.profileLock.Unlock()
238 p, ok := m.profiles[id.String()]
240 return ErrProfileNotFound
242 p.ApplyChange(change)
243 m.profiles[id.String()] = p
247 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
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
262 func (m *memstore) deleteProfile(id uuid.ID) error {
264 defer m.profileLock.Unlock()
265 _, ok := m.profiles[id.String()]
267 return ErrProfileNotFound
269 delete(m.profiles, id.String())
273 func (m *memstore) addLogin(login Login) error {
275 defer m.loginLock.Unlock()
276 _, ok := m.logins[login.Type+":"+login.Value]
278 return ErrLoginAlreadyExists
280 m.logins[login.Type+":"+login.Value] = login
281 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Type+":"+login.Value)
285 func (m *memstore) removeLogin(loginType, value string, profile uuid.ID) error {
287 defer m.loginLock.Unlock()
288 l, ok := m.logins[loginType+":"+value]
290 return ErrLoginNotFound
292 if !l.ProfileID.Equal(profile) {
293 return ErrLoginNotFound
295 delete(m.logins, loginType+":"+value)
297 for p, id := range m.profileLoginLookup[profile.String()] {
298 if id == loginType+":"+value {
304 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
309 func (m *memstore) recordLoginUse(loginType, value string, when time.Time) error {
311 defer m.loginLock.Unlock()
312 l, ok := m.logins[loginType+":"+value]
314 return ErrLoginNotFound
317 m.logins[loginType+":"+value] = l
321 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
323 defer m.loginLock.RUnlock()
324 ids, ok := m.profileLoginLookup[profile.String()]
326 return []Login{}, nil
328 if len(ids) > num+offset {
329 ids = ids[offset : num+offset]
330 } else if len(ids) > offset {
333 return []Login{}, nil
336 for _, id := range ids {
337 login, ok := m.logins[id]
341 logins = append(logins, login)