auth
auth/profile.go
Test our GetClientHandler function, add isAuthError helper. Add a helper that identifies whether the error passed to it is an authentication error or is some other type of error. This is useful fo checking whether or not an internal error occurred while authenticating users. Update all instances where we call our authentication helper to make them use the new error helper. All tests continue to pass. Add a new test case for retrieving a client as an unauthenticated user. This clears the client's secret from the response before sending it. Update the GetClientHandler function to return the secret when the owner of the client used Basic Auth in the request. Add a new test case for retrieving a client as an authenticated user, both the owner and a non-owner user. This makes sure the secret is divulged only in the appropriate cases.
| paddy@27 | 1 package auth |
| paddy@27 | 2 |
| paddy@27 | 3 import ( |
| paddy@99 | 4 "encoding/json" |
| paddy@38 | 5 "errors" |
| paddy@99 | 6 "net/http" |
| paddy@99 | 7 "regexp" |
| paddy@99 | 8 "strings" |
| paddy@27 | 9 "time" |
| paddy@27 | 10 |
| paddy@107 | 11 "code.secondbit.org/uuid.hg" |
| paddy@99 | 12 "github.com/extemporalgenome/slug" |
| paddy@105 | 13 "github.com/gorilla/mux" |
| paddy@27 | 14 ) |
| paddy@27 | 15 |
| paddy@48 | 16 const ( |
| paddy@57 | 17 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive. |
| paddy@48 | 18 MinPassphraseLength = 6 |
| paddy@57 | 19 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive. |
| paddy@48 | 20 MaxPassphraseLength = 64 |
| paddy@69 | 21 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme |
| paddy@69 | 22 CurPassphraseScheme = 1 |
| paddy@99 | 23 // MaxNameLength is the maximum length, in bytes, of a name, exclusive. |
| paddy@99 | 24 MaxNameLength = 64 |
| paddy@99 | 25 // MaxUsernameLength is the maximum length, in bytes, of a username, exclusive. |
| paddy@99 | 26 MaxUsernameLength = 16 |
| paddy@99 | 27 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive. |
| paddy@99 | 28 MaxEmailLength = 64 |
| paddy@48 | 29 ) |
| paddy@48 | 30 |
| paddy@38 | 31 var ( |
| paddy@57 | 32 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first. |
| paddy@57 | 33 ErrNoProfileStore = errors.New("no profileStore was specified for the Context") |
| paddy@57 | 34 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with |
| paddy@57 | 35 // the same ID already exists in the profileStore. |
| paddy@57 | 36 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore") |
| paddy@57 | 37 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore. |
| paddy@57 | 38 ErrProfileNotFound = errors.New("profile not found in profileStore") |
| paddy@57 | 39 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same |
| paddy@57 | 40 // Type and Value already exists in the profileStore. |
| paddy@57 | 41 ErrLoginAlreadyExists = errors.New("login already exists in profileStore") |
| paddy@57 | 42 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore. |
| paddy@57 | 43 ErrLoginNotFound = errors.New("login not found in profileStore") |
| paddy@48 | 44 |
| paddy@57 | 45 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a |
| paddy@57 | 46 // Passphrase, and requires one. |
| paddy@57 | 47 ErrMissingPassphrase = errors.New("missing passphrase") |
| paddy@57 | 48 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain |
| paddy@57 | 49 // a PassphraseReset, and requires one. |
| paddy@57 | 50 ErrMissingPassphraseReset = errors.New("missing passphrase reset") |
| paddy@57 | 51 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not |
| paddy@57 | 52 // contain a PassphraseResetCreated, and requires one. |
| paddy@48 | 53 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp") |
| paddy@57 | 54 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase, |
| paddy@57 | 55 // but the Passphrase is shorter than MinPassphraseLength. |
| paddy@57 | 56 ErrPassphraseTooShort = errors.New("passphrase too short") |
| paddy@57 | 57 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase, |
| paddy@57 | 58 // but the Passphrase is longer than MaxPassphraseLength. |
| paddy@57 | 59 ErrPassphraseTooLong = errors.New("passphrase too long") |
| paddy@99 | 60 |
| paddy@99 | 61 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected |
| paddy@99 | 62 // of being compromised. |
| paddy@99 | 63 ErrProfileCompromised = errors.New("profile compromised") |
| paddy@99 | 64 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain |
| paddy@99 | 65 // duration, to prevent brute force attacks. |
| paddy@99 | 66 ErrProfileLocked = errors.New("profile locked") |
| paddy@38 | 67 ) |
| paddy@38 | 68 |
| paddy@57 | 69 // Profile represents a single user of the service, |
| paddy@57 | 70 // including their authentication information, but not |
| paddy@57 | 71 // including their username or email. |
| paddy@27 | 72 type Profile struct { |
| paddy@105 | 73 ID uuid.ID `json:"id,omitempty"` |
| paddy@105 | 74 Name string `json:"name,omitempty"` |
| paddy@105 | 75 Passphrase string `json:"-"` |
| paddy@105 | 76 Iterations int `json:"-"` |
| paddy@105 | 77 Salt string `json:"-"` |
| paddy@105 | 78 PassphraseScheme int `json:"-"` |
| paddy@105 | 79 Compromised bool `json:"-"` |
| paddy@105 | 80 LockedUntil time.Time `json:"-"` |
| paddy@105 | 81 PassphraseReset string `json:"-"` |
| paddy@105 | 82 PassphraseResetCreated time.Time `json:"-"` |
| paddy@105 | 83 Created time.Time `json:"created,omitempty"` |
| paddy@105 | 84 LastSeen time.Time `json:"last_seen,omitempty"` |
| paddy@38 | 85 } |
| paddy@38 | 86 |
| paddy@57 | 87 // ApplyChange applies the properties of the passed ProfileChange |
| paddy@57 | 88 // to the Profile it is called on. |
| paddy@38 | 89 func (p *Profile) ApplyChange(change ProfileChange) { |
| paddy@38 | 90 if change.Name != nil { |
| paddy@38 | 91 p.Name = *change.Name |
| paddy@38 | 92 } |
| paddy@38 | 93 if change.Passphrase != nil { |
| paddy@38 | 94 p.Passphrase = *change.Passphrase |
| paddy@38 | 95 } |
| paddy@38 | 96 if change.Iterations != nil { |
| paddy@38 | 97 p.Iterations = *change.Iterations |
| paddy@38 | 98 } |
| paddy@38 | 99 if change.Salt != nil { |
| paddy@38 | 100 p.Salt = *change.Salt |
| paddy@38 | 101 } |
| paddy@38 | 102 if change.PassphraseScheme != nil { |
| paddy@38 | 103 p.PassphraseScheme = *change.PassphraseScheme |
| paddy@38 | 104 } |
| paddy@38 | 105 if change.Compromised != nil { |
| paddy@38 | 106 p.Compromised = *change.Compromised |
| paddy@38 | 107 } |
| paddy@38 | 108 if change.LockedUntil != nil { |
| paddy@38 | 109 p.LockedUntil = *change.LockedUntil |
| paddy@38 | 110 } |
| paddy@38 | 111 if change.PassphraseReset != nil { |
| paddy@38 | 112 p.PassphraseReset = *change.PassphraseReset |
| paddy@38 | 113 } |
| paddy@38 | 114 if change.PassphraseResetCreated != nil { |
| paddy@38 | 115 p.PassphraseResetCreated = *change.PassphraseResetCreated |
| paddy@38 | 116 } |
| paddy@38 | 117 if change.LastSeen != nil { |
| paddy@38 | 118 p.LastSeen = *change.LastSeen |
| paddy@38 | 119 } |
| paddy@38 | 120 } |
| paddy@38 | 121 |
| paddy@57 | 122 // ApplyBulkChange applies the properties of the passed BulkProfileChange |
| paddy@57 | 123 // to the Profile it is called on. |
| paddy@44 | 124 func (p *Profile) ApplyBulkChange(change BulkProfileChange) { |
| paddy@44 | 125 if change.Compromised != nil { |
| paddy@44 | 126 p.Compromised = *change.Compromised |
| paddy@44 | 127 } |
| paddy@44 | 128 } |
| paddy@44 | 129 |
| paddy@57 | 130 // ProfileChange represents a single atomic change to a Profile's mutable data. |
| paddy@38 | 131 type ProfileChange struct { |
| paddy@38 | 132 Name *string |
| paddy@38 | 133 Passphrase *string |
| paddy@69 | 134 Iterations *int |
| paddy@38 | 135 Salt *string |
| paddy@38 | 136 PassphraseScheme *int |
| paddy@38 | 137 Compromised *bool |
| paddy@38 | 138 LockedUntil *time.Time |
| paddy@38 | 139 PassphraseReset *string |
| paddy@38 | 140 PassphraseResetCreated *time.Time |
| paddy@38 | 141 LastSeen *time.Time |
| paddy@38 | 142 } |
| paddy@38 | 143 |
| paddy@57 | 144 // Validate checks the ProfileChange it is called on |
| paddy@57 | 145 // and asserts its internal validity, or lack thereof. |
| paddy@57 | 146 // A descriptive error will be returned in the case of |
| paddy@57 | 147 // an invalid change. |
| paddy@38 | 148 func (c ProfileChange) Validate() error { |
| paddy@48 | 149 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 | 150 return ErrEmptyChange |
| paddy@48 | 151 } |
| paddy@48 | 152 if c.PassphraseScheme != nil && c.Passphrase == nil { |
| paddy@48 | 153 return ErrMissingPassphrase |
| paddy@48 | 154 } |
| paddy@48 | 155 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil { |
| paddy@48 | 156 return ErrMissingPassphraseResetCreated |
| paddy@48 | 157 } |
| paddy@48 | 158 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil { |
| paddy@48 | 159 return ErrMissingPassphraseReset |
| paddy@48 | 160 } |
| paddy@48 | 161 if c.Salt != nil && c.Passphrase == nil { |
| paddy@48 | 162 return ErrMissingPassphrase |
| paddy@48 | 163 } |
| paddy@48 | 164 if c.Iterations != nil && c.Passphrase == nil { |
| paddy@48 | 165 return ErrMissingPassphrase |
| paddy@48 | 166 } |
| paddy@48 | 167 if c.Passphrase != nil && len(*c.Passphrase) < MinPassphraseLength { |
| paddy@48 | 168 return ErrPassphraseTooShort |
| paddy@48 | 169 } |
| paddy@48 | 170 if c.Passphrase != nil && len(*c.Passphrase) > MaxPassphraseLength { |
| paddy@48 | 171 return ErrPassphraseTooLong |
| paddy@48 | 172 } |
| paddy@38 | 173 return nil |
| paddy@27 | 174 } |
| paddy@27 | 175 |
| paddy@57 | 176 // BulkProfileChange represents a single atomic change to many Profiles' mutable data. |
| paddy@57 | 177 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the |
| paddy@57 | 178 // ProfileChange values across many Profiles all at once. |
| paddy@44 | 179 type BulkProfileChange struct { |
| paddy@44 | 180 Compromised *bool |
| paddy@44 | 181 } |
| paddy@44 | 182 |
| paddy@57 | 183 // Validate checks the BulkProfileChange it is called on |
| paddy@57 | 184 // and asserts its internal validity, or lack thereof. |
| paddy@57 | 185 // A descriptive error will be returned in the case of an |
| paddy@57 | 186 // invalid change. |
| paddy@44 | 187 func (b BulkProfileChange) Validate() error { |
| paddy@48 | 188 if b.Compromised == nil { |
| paddy@48 | 189 return ErrEmptyChange |
| paddy@48 | 190 } |
| paddy@44 | 191 return nil |
| paddy@44 | 192 } |
| paddy@44 | 193 |
| paddy@57 | 194 // Login represents a single human-friendly identifier for |
| paddy@57 | 195 // a given Profile that can be used to log into that Profile. |
| paddy@57 | 196 // Each Profile may only have one Login for each Type. |
| paddy@27 | 197 type Login struct { |
| paddy@105 | 198 Type string `json:"type,omitempty"` |
| paddy@105 | 199 Value string `json:"value,omitempty"` |
| paddy@105 | 200 ProfileID uuid.ID `json:"profile_id,omitempty"` |
| paddy@105 | 201 Created time.Time `json:"created,omitempty"` |
| paddy@105 | 202 LastUsed time.Time `json:"last_used,omitempty"` |
| paddy@27 | 203 } |
| paddy@27 | 204 |
| paddy@99 | 205 type newProfileRequest struct { |
| paddy@99 | 206 Username string `json:"username"` |
| paddy@99 | 207 Email string `json:"email"` |
| paddy@99 | 208 Passphrase string `json:"passphrase"` |
| paddy@99 | 209 Name string `json:"name"` |
| paddy@99 | 210 } |
| paddy@99 | 211 |
| paddy@99 | 212 func validateNewProfileRequest(req *newProfileRequest) []requestError { |
| paddy@99 | 213 errors := []requestError{} |
| paddy@99 | 214 req.Name = strings.TrimSpace(req.Name) |
| paddy@99 | 215 req.Email = strings.TrimSpace(req.Email) |
| paddy@99 | 216 req.Username = slug.SlugAscii(strings.TrimSpace(req.Username)) |
| paddy@99 | 217 if len(req.Passphrase) < MinPassphraseLength { |
| paddy@99 | 218 errors = append(errors, requestError{ |
| paddy@99 | 219 Slug: requestErrInsufficient, |
| paddy@99 | 220 Field: "/passphrase", |
| paddy@99 | 221 }) |
| paddy@99 | 222 } |
| paddy@99 | 223 if len(req.Passphrase) > MaxPassphraseLength { |
| paddy@99 | 224 errors = append(errors, requestError{ |
| paddy@99 | 225 Slug: requestErrOverflow, |
| paddy@99 | 226 Field: "/passphrase", |
| paddy@99 | 227 }) |
| paddy@99 | 228 } |
| paddy@99 | 229 if len(req.Name) > MaxNameLength { |
| paddy@99 | 230 errors = append(errors, requestError{ |
| paddy@99 | 231 Slug: requestErrOverflow, |
| paddy@99 | 232 Field: "/name", |
| paddy@99 | 233 }) |
| paddy@99 | 234 } |
| paddy@99 | 235 if len(req.Username) > MaxUsernameLength { |
| paddy@99 | 236 errors = append(errors, requestError{ |
| paddy@99 | 237 Slug: requestErrOverflow, |
| paddy@99 | 238 Field: "/username", |
| paddy@99 | 239 }) |
| paddy@99 | 240 } |
| paddy@99 | 241 if req.Email == "" { |
| paddy@99 | 242 errors = append(errors, requestError{ |
| paddy@99 | 243 Slug: requestErrMissing, |
| paddy@99 | 244 Field: "/email", |
| paddy@99 | 245 }) |
| paddy@99 | 246 } |
| paddy@99 | 247 if len(req.Email) > MaxEmailLength { |
| paddy@99 | 248 errors = append(errors, requestError{ |
| paddy@99 | 249 Slug: requestErrOverflow, |
| paddy@99 | 250 Field: "/email", |
| paddy@99 | 251 }) |
| paddy@99 | 252 } |
| paddy@99 | 253 re := regexp.MustCompile(".+@.+\\..+") |
| paddy@99 | 254 if !re.Match([]byte(req.Email)) { |
| paddy@99 | 255 errors = append(errors, requestError{ |
| paddy@105 | 256 Slug: requestErrInvalidFormat, |
| paddy@99 | 257 Field: "/email", |
| paddy@99 | 258 }) |
| paddy@99 | 259 } |
| paddy@99 | 260 return errors |
| paddy@99 | 261 } |
| paddy@99 | 262 |
| paddy@57 | 263 type profileStore interface { |
| paddy@57 | 264 getProfileByID(id uuid.ID) (Profile, error) |
| paddy@69 | 265 getProfileByLogin(value string) (Profile, error) |
| paddy@57 | 266 saveProfile(profile Profile) error |
| paddy@57 | 267 updateProfile(id uuid.ID, change ProfileChange) error |
| paddy@57 | 268 updateProfiles(ids []uuid.ID, change BulkProfileChange) error |
| paddy@57 | 269 deleteProfile(id uuid.ID) error |
| paddy@44 | 270 |
| paddy@57 | 271 addLogin(login Login) error |
| paddy@69 | 272 removeLogin(value string, profile uuid.ID) error |
| paddy@69 | 273 recordLoginUse(value string, when time.Time) error |
| paddy@57 | 274 listLogins(profile uuid.ID, num, offset int) ([]Login, error) |
| paddy@38 | 275 } |
| paddy@27 | 276 |
| paddy@57 | 277 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) { |
| paddy@38 | 278 m.profileLock.RLock() |
| paddy@38 | 279 defer m.profileLock.RUnlock() |
| paddy@38 | 280 p, ok := m.profiles[id.String()] |
| paddy@38 | 281 if !ok { |
| paddy@38 | 282 return Profile{}, ErrProfileNotFound |
| paddy@38 | 283 } |
| paddy@38 | 284 return p, nil |
| paddy@27 | 285 } |
| paddy@38 | 286 |
| paddy@69 | 287 func (m *memstore) getProfileByLogin(value string) (Profile, error) { |
| paddy@44 | 288 m.loginLock.RLock() |
| paddy@44 | 289 defer m.loginLock.RUnlock() |
| paddy@69 | 290 login, ok := m.logins[value] |
| paddy@44 | 291 if !ok { |
| paddy@44 | 292 return Profile{}, ErrLoginNotFound |
| paddy@44 | 293 } |
| paddy@44 | 294 m.profileLock.RLock() |
| paddy@44 | 295 defer m.profileLock.RUnlock() |
| paddy@44 | 296 profile, ok := m.profiles[login.ProfileID.String()] |
| paddy@44 | 297 if !ok { |
| paddy@44 | 298 return Profile{}, ErrProfileNotFound |
| paddy@44 | 299 } |
| paddy@44 | 300 return profile, nil |
| paddy@38 | 301 } |
| paddy@38 | 302 |
| paddy@57 | 303 func (m *memstore) saveProfile(profile Profile) error { |
| paddy@38 | 304 m.profileLock.Lock() |
| paddy@38 | 305 defer m.profileLock.Unlock() |
| paddy@38 | 306 _, ok := m.profiles[profile.ID.String()] |
| paddy@38 | 307 if ok { |
| paddy@38 | 308 return ErrProfileAlreadyExists |
| paddy@38 | 309 } |
| paddy@38 | 310 m.profiles[profile.ID.String()] = profile |
| paddy@38 | 311 return nil |
| paddy@38 | 312 } |
| paddy@38 | 313 |
| paddy@57 | 314 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error { |
| paddy@38 | 315 m.profileLock.Lock() |
| paddy@38 | 316 defer m.profileLock.Unlock() |
| paddy@38 | 317 p, ok := m.profiles[id.String()] |
| paddy@38 | 318 if !ok { |
| paddy@38 | 319 return ErrProfileNotFound |
| paddy@38 | 320 } |
| paddy@38 | 321 p.ApplyChange(change) |
| paddy@38 | 322 m.profiles[id.String()] = p |
| paddy@38 | 323 return nil |
| paddy@38 | 324 } |
| paddy@38 | 325 |
| paddy@57 | 326 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error { |
| paddy@44 | 327 m.profileLock.Lock() |
| paddy@44 | 328 defer m.profileLock.Unlock() |
| paddy@44 | 329 for id, profile := range m.profiles { |
| paddy@44 | 330 for _, i := range ids { |
| paddy@44 | 331 if id == i.String() { |
| paddy@44 | 332 profile.ApplyBulkChange(change) |
| paddy@44 | 333 m.profiles[id] = profile |
| paddy@44 | 334 break |
| paddy@44 | 335 } |
| paddy@44 | 336 } |
| paddy@44 | 337 } |
| paddy@44 | 338 return nil |
| paddy@44 | 339 } |
| paddy@44 | 340 |
| paddy@57 | 341 func (m *memstore) deleteProfile(id uuid.ID) error { |
| paddy@38 | 342 m.profileLock.Lock() |
| paddy@38 | 343 defer m.profileLock.Unlock() |
| paddy@38 | 344 _, ok := m.profiles[id.String()] |
| paddy@38 | 345 if !ok { |
| paddy@38 | 346 return ErrProfileNotFound |
| paddy@38 | 347 } |
| paddy@38 | 348 delete(m.profiles, id.String()) |
| paddy@38 | 349 return nil |
| paddy@38 | 350 } |
| paddy@40 | 351 |
| paddy@57 | 352 func (m *memstore) addLogin(login Login) error { |
| paddy@44 | 353 m.loginLock.Lock() |
| paddy@44 | 354 defer m.loginLock.Unlock() |
| paddy@69 | 355 _, ok := m.logins[login.Value] |
| paddy@44 | 356 if ok { |
| paddy@44 | 357 return ErrLoginAlreadyExists |
| paddy@44 | 358 } |
| paddy@69 | 359 m.logins[login.Value] = login |
| paddy@69 | 360 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value) |
| paddy@44 | 361 return nil |
| paddy@44 | 362 } |
| paddy@44 | 363 |
| paddy@69 | 364 func (m *memstore) removeLogin(value string, profile uuid.ID) error { |
| paddy@44 | 365 m.loginLock.Lock() |
| paddy@44 | 366 defer m.loginLock.Unlock() |
| paddy@69 | 367 l, ok := m.logins[value] |
| paddy@44 | 368 if !ok { |
| paddy@44 | 369 return ErrLoginNotFound |
| paddy@44 | 370 } |
| paddy@44 | 371 if !l.ProfileID.Equal(profile) { |
| paddy@44 | 372 return ErrLoginNotFound |
| paddy@44 | 373 } |
| paddy@69 | 374 delete(m.logins, value) |
| paddy@44 | 375 pos := -1 |
| paddy@44 | 376 for p, id := range m.profileLoginLookup[profile.String()] { |
| paddy@69 | 377 if id == value { |
| paddy@44 | 378 pos = p |
| paddy@44 | 379 break |
| paddy@44 | 380 } |
| paddy@44 | 381 } |
| paddy@44 | 382 if pos >= 0 { |
| paddy@44 | 383 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...) |
| paddy@44 | 384 } |
| paddy@44 | 385 return nil |
| paddy@44 | 386 } |
| paddy@44 | 387 |
| paddy@69 | 388 func (m *memstore) recordLoginUse(value string, when time.Time) error { |
| paddy@44 | 389 m.loginLock.Lock() |
| paddy@44 | 390 defer m.loginLock.Unlock() |
| paddy@69 | 391 l, ok := m.logins[value] |
| paddy@44 | 392 if !ok { |
| paddy@44 | 393 return ErrLoginNotFound |
| paddy@44 | 394 } |
| paddy@44 | 395 l.LastUsed = when |
| paddy@69 | 396 m.logins[value] = l |
| paddy@44 | 397 return nil |
| paddy@44 | 398 } |
| paddy@44 | 399 |
| paddy@57 | 400 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) { |
| paddy@44 | 401 m.loginLock.RLock() |
| paddy@44 | 402 defer m.loginLock.RUnlock() |
| paddy@44 | 403 ids, ok := m.profileLoginLookup[profile.String()] |
| paddy@44 | 404 if !ok { |
| paddy@44 | 405 return []Login{}, nil |
| paddy@44 | 406 } |
| paddy@44 | 407 if len(ids) > num+offset { |
| paddy@44 | 408 ids = ids[offset : num+offset] |
| paddy@44 | 409 } else if len(ids) > offset { |
| paddy@44 | 410 ids = ids[offset:] |
| paddy@44 | 411 } else { |
| paddy@44 | 412 return []Login{}, nil |
| paddy@44 | 413 } |
| paddy@44 | 414 logins := []Login{} |
| paddy@44 | 415 for _, id := range ids { |
| paddy@44 | 416 login, ok := m.logins[id] |
| paddy@44 | 417 if !ok { |
| paddy@44 | 418 continue |
| paddy@44 | 419 } |
| paddy@44 | 420 logins = append(logins, login) |
| paddy@44 | 421 } |
| paddy@44 | 422 return logins, nil |
| paddy@44 | 423 } |
| paddy@99 | 424 |
| paddy@105 | 425 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval. |
| paddy@105 | 426 func RegisterProfileHandlers(r *mux.Router, context Context) { |
| paddy@105 | 427 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST") |
| paddy@128 | 428 // BUG(paddy): We need to implement a handler that will return information about a profile or set of profiles. |
| paddy@128 | 429 // BUG(paddy): We need to implement a handler that will update a profile. |
| paddy@128 | 430 // BUG(paddy): We need to implement a handler that will delete a profile. What happens to clients/tokens/grants/sessions when a profile is deleted? |
| paddy@128 | 431 // BUG(paddy): We need to implement a handler that will add a login to a profile. |
| paddy@128 | 432 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login? |
| paddy@128 | 433 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile. |
| paddy@105 | 434 } |
| paddy@105 | 435 |
| paddy@99 | 436 // CreateProfileHandler is an HTTP handler for registering new profiles. |
| paddy@99 | 437 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@99 | 438 scheme, ok := passphraseSchemes[CurPassphraseScheme] |
| paddy@99 | 439 if !ok { |
| paddy@105 | 440 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 441 return |
| paddy@99 | 442 } |
| paddy@99 | 443 var req newProfileRequest |
| paddy@99 | 444 errors := []requestError{} |
| paddy@99 | 445 decoder := json.NewDecoder(r.Body) |
| paddy@99 | 446 err := decoder.Decode(&req) |
| paddy@99 | 447 if err != nil { |
| paddy@105 | 448 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@99 | 449 return |
| paddy@99 | 450 } |
| paddy@99 | 451 errors = append(errors, validateNewProfileRequest(&req)...) |
| paddy@99 | 452 if len(errors) > 0 { |
| paddy@105 | 453 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@99 | 454 return |
| paddy@99 | 455 } |
| paddy@99 | 456 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations) |
| paddy@99 | 457 if err != nil { |
| paddy@105 | 458 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 459 return |
| paddy@99 | 460 } |
| paddy@99 | 461 profile := Profile{ |
| paddy@99 | 462 ID: uuid.NewID(), |
| paddy@99 | 463 Name: req.Name, |
| paddy@99 | 464 Passphrase: string(passphrase), |
| paddy@99 | 465 Iterations: context.config.iterations, |
| paddy@99 | 466 Salt: string(salt), |
| paddy@99 | 467 PassphraseScheme: CurPassphraseScheme, |
| paddy@99 | 468 Created: time.Now(), |
| paddy@99 | 469 LastSeen: time.Now(), |
| paddy@99 | 470 } |
| paddy@99 | 471 err = context.SaveProfile(profile) |
| paddy@99 | 472 if err != nil { |
| paddy@105 | 473 if err == ErrProfileAlreadyExists { |
| paddy@105 | 474 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}}) |
| paddy@105 | 475 return |
| paddy@105 | 476 } |
| paddy@105 | 477 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 478 return |
| paddy@99 | 479 } |
| paddy@99 | 480 logins := []Login{} |
| paddy@99 | 481 login := Login{ |
| paddy@99 | 482 Type: "email", |
| paddy@99 | 483 Value: req.Email, |
| paddy@99 | 484 Created: profile.Created, |
| paddy@99 | 485 LastUsed: profile.Created, |
| paddy@99 | 486 ProfileID: profile.ID, |
| paddy@99 | 487 } |
| paddy@99 | 488 err = context.AddLogin(login) |
| paddy@99 | 489 if err != nil { |
| paddy@105 | 490 if err == ErrLoginAlreadyExists { |
| paddy@105 | 491 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}}) |
| paddy@105 | 492 return |
| paddy@105 | 493 } |
| paddy@105 | 494 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 495 return |
| paddy@99 | 496 } |
| paddy@99 | 497 logins = append(logins, login) |
| paddy@99 | 498 if req.Username != "" { |
| paddy@99 | 499 login.Type = "username" |
| paddy@99 | 500 login.Value = req.Username |
| paddy@99 | 501 err = context.AddLogin(login) |
| paddy@99 | 502 if err != nil { |
| paddy@105 | 503 if err == ErrLoginAlreadyExists { |
| paddy@105 | 504 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/username"}}}) |
| paddy@105 | 505 return |
| paddy@105 | 506 } |
| paddy@105 | 507 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 508 return |
| paddy@99 | 509 } |
| paddy@99 | 510 logins = append(logins, login) |
| paddy@99 | 511 } |
| paddy@105 | 512 resp := response{ |
| paddy@105 | 513 Logins: logins, |
| paddy@105 | 514 Profiles: []Profile{profile}, |
| paddy@105 | 515 } |
| paddy@105 | 516 encode(w, r, http.StatusCreated, resp) |
| paddy@99 | 517 // TODO(paddy): should we kick off the email validation flow? |
| paddy@99 | 518 } |