auth
auth/profile.go
Wire up the postgres database for authd. Have authd use the AUTH_PG_DB environment variable to detect support for the postgres *Stores, and if postgres is supported, use it. If postgres isn't supported, fall back on the in-memory store. Also create-if-not-exists the test scopes, instead of panicking when the scope already exists.
| paddy@27 | 1 package auth |
| paddy@27 | 2 |
| paddy@27 | 3 import ( |
| paddy@99 | 4 "encoding/json" |
| paddy@38 | 5 "errors" |
| paddy@149 | 6 "log" |
| paddy@99 | 7 "net/http" |
| paddy@99 | 8 "regexp" |
| paddy@99 | 9 "strings" |
| paddy@27 | 10 "time" |
| paddy@27 | 11 |
| paddy@107 | 12 "code.secondbit.org/uuid.hg" |
| paddy@99 | 13 "github.com/extemporalgenome/slug" |
| paddy@105 | 14 "github.com/gorilla/mux" |
| paddy@27 | 15 ) |
| paddy@27 | 16 |
| paddy@48 | 17 const ( |
| paddy@57 | 18 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive. |
| paddy@48 | 19 MinPassphraseLength = 6 |
| paddy@57 | 20 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive. |
| paddy@48 | 21 MaxPassphraseLength = 64 |
| paddy@69 | 22 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme |
| paddy@69 | 23 CurPassphraseScheme = 1 |
| paddy@99 | 24 // MaxNameLength is the maximum length, in bytes, of a name, exclusive. |
| paddy@99 | 25 MaxNameLength = 64 |
| paddy@99 | 26 // MaxUsernameLength is the maximum length, in bytes, of a username, exclusive. |
| paddy@99 | 27 MaxUsernameLength = 16 |
| paddy@99 | 28 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive. |
| paddy@99 | 29 MaxEmailLength = 64 |
| paddy@48 | 30 ) |
| paddy@48 | 31 |
| paddy@38 | 32 var ( |
| paddy@57 | 33 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first. |
| paddy@57 | 34 ErrNoProfileStore = errors.New("no profileStore was specified for the Context") |
| paddy@57 | 35 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with |
| paddy@57 | 36 // the same ID already exists in the profileStore. |
| paddy@57 | 37 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore") |
| paddy@57 | 38 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore. |
| paddy@57 | 39 ErrProfileNotFound = errors.New("profile not found in profileStore") |
| paddy@57 | 40 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same |
| paddy@57 | 41 // Type and Value already exists in the profileStore. |
| paddy@57 | 42 ErrLoginAlreadyExists = errors.New("login already exists in profileStore") |
| paddy@57 | 43 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore. |
| paddy@57 | 44 ErrLoginNotFound = errors.New("login not found in profileStore") |
| paddy@48 | 45 |
| paddy@57 | 46 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a |
| paddy@57 | 47 // Passphrase, and requires one. |
| paddy@57 | 48 ErrMissingPassphrase = errors.New("missing passphrase") |
| paddy@57 | 49 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain |
| paddy@57 | 50 // a PassphraseReset, and requires one. |
| paddy@57 | 51 ErrMissingPassphraseReset = errors.New("missing passphrase reset") |
| paddy@57 | 52 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not |
| paddy@57 | 53 // contain a PassphraseResetCreated, and requires one. |
| paddy@48 | 54 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp") |
| paddy@57 | 55 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase, |
| paddy@57 | 56 // but the Passphrase is shorter than MinPassphraseLength. |
| paddy@57 | 57 ErrPassphraseTooShort = errors.New("passphrase too short") |
| paddy@57 | 58 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase, |
| paddy@57 | 59 // but the Passphrase is longer than MaxPassphraseLength. |
| paddy@57 | 60 ErrPassphraseTooLong = errors.New("passphrase too long") |
| paddy@99 | 61 |
| paddy@99 | 62 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected |
| paddy@99 | 63 // of being compromised. |
| paddy@99 | 64 ErrProfileCompromised = errors.New("profile compromised") |
| paddy@99 | 65 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain |
| paddy@99 | 66 // duration, to prevent brute force attacks. |
| paddy@99 | 67 ErrProfileLocked = errors.New("profile locked") |
| paddy@38 | 68 ) |
| paddy@38 | 69 |
| paddy@57 | 70 // Profile represents a single user of the service, |
| paddy@57 | 71 // including their authentication information, but not |
| paddy@57 | 72 // including their username or email. |
| paddy@27 | 73 type Profile struct { |
| paddy@105 | 74 ID uuid.ID `json:"id,omitempty"` |
| paddy@105 | 75 Name string `json:"name,omitempty"` |
| paddy@105 | 76 Passphrase string `json:"-"` |
| paddy@105 | 77 Iterations int `json:"-"` |
| paddy@105 | 78 Salt string `json:"-"` |
| paddy@105 | 79 PassphraseScheme int `json:"-"` |
| paddy@105 | 80 Compromised bool `json:"-"` |
| paddy@105 | 81 LockedUntil time.Time `json:"-"` |
| paddy@105 | 82 PassphraseReset string `json:"-"` |
| paddy@105 | 83 PassphraseResetCreated time.Time `json:"-"` |
| paddy@105 | 84 Created time.Time `json:"created,omitempty"` |
| paddy@105 | 85 LastSeen time.Time `json:"last_seen,omitempty"` |
| paddy@148 | 86 Deleted bool `json:"deleted,omitempty"` |
| paddy@38 | 87 } |
| paddy@38 | 88 |
| paddy@57 | 89 // ApplyChange applies the properties of the passed ProfileChange |
| paddy@57 | 90 // to the Profile it is called on. |
| paddy@38 | 91 func (p *Profile) ApplyChange(change ProfileChange) { |
| paddy@38 | 92 if change.Name != nil { |
| paddy@38 | 93 p.Name = *change.Name |
| paddy@38 | 94 } |
| paddy@38 | 95 if change.Passphrase != nil { |
| paddy@38 | 96 p.Passphrase = *change.Passphrase |
| paddy@38 | 97 } |
| paddy@38 | 98 if change.Iterations != nil { |
| paddy@38 | 99 p.Iterations = *change.Iterations |
| paddy@38 | 100 } |
| paddy@38 | 101 if change.Salt != nil { |
| paddy@38 | 102 p.Salt = *change.Salt |
| paddy@38 | 103 } |
| paddy@38 | 104 if change.PassphraseScheme != nil { |
| paddy@38 | 105 p.PassphraseScheme = *change.PassphraseScheme |
| paddy@38 | 106 } |
| paddy@38 | 107 if change.Compromised != nil { |
| paddy@38 | 108 p.Compromised = *change.Compromised |
| paddy@38 | 109 } |
| paddy@38 | 110 if change.LockedUntil != nil { |
| paddy@38 | 111 p.LockedUntil = *change.LockedUntil |
| paddy@38 | 112 } |
| paddy@38 | 113 if change.PassphraseReset != nil { |
| paddy@38 | 114 p.PassphraseReset = *change.PassphraseReset |
| paddy@38 | 115 } |
| paddy@38 | 116 if change.PassphraseResetCreated != nil { |
| paddy@38 | 117 p.PassphraseResetCreated = *change.PassphraseResetCreated |
| paddy@38 | 118 } |
| paddy@38 | 119 if change.LastSeen != nil { |
| paddy@38 | 120 p.LastSeen = *change.LastSeen |
| paddy@38 | 121 } |
| paddy@148 | 122 if change.Deleted != nil { |
| paddy@148 | 123 p.Deleted = *change.Deleted |
| paddy@148 | 124 } |
| paddy@38 | 125 } |
| paddy@38 | 126 |
| paddy@57 | 127 // ApplyBulkChange applies the properties of the passed BulkProfileChange |
| paddy@57 | 128 // to the Profile it is called on. |
| paddy@44 | 129 func (p *Profile) ApplyBulkChange(change BulkProfileChange) { |
| paddy@44 | 130 if change.Compromised != nil { |
| paddy@44 | 131 p.Compromised = *change.Compromised |
| paddy@44 | 132 } |
| paddy@44 | 133 } |
| paddy@44 | 134 |
| paddy@57 | 135 // ProfileChange represents a single atomic change to a Profile's mutable data. |
| paddy@38 | 136 type ProfileChange struct { |
| paddy@38 | 137 Name *string |
| paddy@38 | 138 Passphrase *string |
| paddy@69 | 139 Iterations *int |
| paddy@38 | 140 Salt *string |
| paddy@38 | 141 PassphraseScheme *int |
| paddy@38 | 142 Compromised *bool |
| paddy@38 | 143 LockedUntil *time.Time |
| paddy@38 | 144 PassphraseReset *string |
| paddy@38 | 145 PassphraseResetCreated *time.Time |
| paddy@38 | 146 LastSeen *time.Time |
| paddy@148 | 147 Deleted *bool |
| paddy@38 | 148 } |
| paddy@38 | 149 |
| paddy@149 | 150 func (c ProfileChange) Empty() bool { |
| paddy@149 | 151 return (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 && c.Deleted == nil) |
| paddy@149 | 152 } |
| paddy@149 | 153 |
| paddy@57 | 154 // Validate checks the ProfileChange it is called on |
| paddy@57 | 155 // and asserts its internal validity, or lack thereof. |
| paddy@57 | 156 // A descriptive error will be returned in the case of |
| paddy@57 | 157 // an invalid change. |
| paddy@38 | 158 func (c ProfileChange) Validate() error { |
| paddy@149 | 159 if c.Empty() { |
| paddy@48 | 160 return ErrEmptyChange |
| paddy@48 | 161 } |
| paddy@48 | 162 if c.PassphraseScheme != nil && c.Passphrase == nil { |
| paddy@48 | 163 return ErrMissingPassphrase |
| paddy@48 | 164 } |
| paddy@48 | 165 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil { |
| paddy@48 | 166 return ErrMissingPassphraseResetCreated |
| paddy@48 | 167 } |
| paddy@48 | 168 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil { |
| paddy@48 | 169 return ErrMissingPassphraseReset |
| paddy@48 | 170 } |
| paddy@48 | 171 if c.Salt != nil && c.Passphrase == nil { |
| paddy@48 | 172 return ErrMissingPassphrase |
| paddy@48 | 173 } |
| paddy@48 | 174 if c.Iterations != nil && c.Passphrase == nil { |
| paddy@48 | 175 return ErrMissingPassphrase |
| paddy@48 | 176 } |
| paddy@38 | 177 return nil |
| paddy@27 | 178 } |
| paddy@27 | 179 |
| paddy@57 | 180 // BulkProfileChange represents a single atomic change to many Profiles' mutable data. |
| paddy@57 | 181 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the |
| paddy@57 | 182 // ProfileChange values across many Profiles all at once. |
| paddy@44 | 183 type BulkProfileChange struct { |
| paddy@44 | 184 Compromised *bool |
| paddy@44 | 185 } |
| paddy@44 | 186 |
| paddy@149 | 187 func (b BulkProfileChange) Empty() bool { |
| paddy@149 | 188 return b.Compromised == nil |
| paddy@149 | 189 } |
| paddy@149 | 190 |
| paddy@57 | 191 // Validate checks the BulkProfileChange it is called on |
| paddy@57 | 192 // and asserts its internal validity, or lack thereof. |
| paddy@57 | 193 // A descriptive error will be returned in the case of an |
| paddy@57 | 194 // invalid change. |
| paddy@44 | 195 func (b BulkProfileChange) Validate() error { |
| paddy@149 | 196 if b.Empty() { |
| paddy@48 | 197 return ErrEmptyChange |
| paddy@48 | 198 } |
| paddy@44 | 199 return nil |
| paddy@44 | 200 } |
| paddy@44 | 201 |
| paddy@57 | 202 // Login represents a single human-friendly identifier for |
| paddy@57 | 203 // a given Profile that can be used to log into that Profile. |
| paddy@57 | 204 // Each Profile may only have one Login for each Type. |
| paddy@27 | 205 type Login struct { |
| paddy@105 | 206 Type string `json:"type,omitempty"` |
| paddy@105 | 207 Value string `json:"value,omitempty"` |
| paddy@105 | 208 ProfileID uuid.ID `json:"profile_id,omitempty"` |
| paddy@105 | 209 Created time.Time `json:"created,omitempty"` |
| paddy@105 | 210 LastUsed time.Time `json:"last_used,omitempty"` |
| paddy@27 | 211 } |
| paddy@27 | 212 |
| paddy@99 | 213 type newProfileRequest struct { |
| paddy@99 | 214 Username string `json:"username"` |
| paddy@99 | 215 Email string `json:"email"` |
| paddy@99 | 216 Passphrase string `json:"passphrase"` |
| paddy@99 | 217 Name string `json:"name"` |
| paddy@99 | 218 } |
| paddy@99 | 219 |
| paddy@99 | 220 func validateNewProfileRequest(req *newProfileRequest) []requestError { |
| paddy@99 | 221 errors := []requestError{} |
| paddy@99 | 222 req.Name = strings.TrimSpace(req.Name) |
| paddy@99 | 223 req.Email = strings.TrimSpace(req.Email) |
| paddy@99 | 224 req.Username = slug.SlugAscii(strings.TrimSpace(req.Username)) |
| paddy@99 | 225 if len(req.Passphrase) < MinPassphraseLength { |
| paddy@99 | 226 errors = append(errors, requestError{ |
| paddy@99 | 227 Slug: requestErrInsufficient, |
| paddy@99 | 228 Field: "/passphrase", |
| paddy@99 | 229 }) |
| paddy@99 | 230 } |
| paddy@99 | 231 if len(req.Passphrase) > MaxPassphraseLength { |
| paddy@99 | 232 errors = append(errors, requestError{ |
| paddy@99 | 233 Slug: requestErrOverflow, |
| paddy@99 | 234 Field: "/passphrase", |
| paddy@99 | 235 }) |
| paddy@99 | 236 } |
| paddy@99 | 237 if len(req.Name) > MaxNameLength { |
| paddy@99 | 238 errors = append(errors, requestError{ |
| paddy@99 | 239 Slug: requestErrOverflow, |
| paddy@99 | 240 Field: "/name", |
| paddy@99 | 241 }) |
| paddy@99 | 242 } |
| paddy@99 | 243 if len(req.Username) > MaxUsernameLength { |
| paddy@99 | 244 errors = append(errors, requestError{ |
| paddy@99 | 245 Slug: requestErrOverflow, |
| paddy@99 | 246 Field: "/username", |
| paddy@99 | 247 }) |
| paddy@99 | 248 } |
| paddy@99 | 249 if req.Email == "" { |
| paddy@99 | 250 errors = append(errors, requestError{ |
| paddy@99 | 251 Slug: requestErrMissing, |
| paddy@99 | 252 Field: "/email", |
| paddy@99 | 253 }) |
| paddy@99 | 254 } |
| paddy@99 | 255 if len(req.Email) > MaxEmailLength { |
| paddy@99 | 256 errors = append(errors, requestError{ |
| paddy@99 | 257 Slug: requestErrOverflow, |
| paddy@99 | 258 Field: "/email", |
| paddy@99 | 259 }) |
| paddy@99 | 260 } |
| paddy@99 | 261 re := regexp.MustCompile(".+@.+\\..+") |
| paddy@99 | 262 if !re.Match([]byte(req.Email)) { |
| paddy@99 | 263 errors = append(errors, requestError{ |
| paddy@105 | 264 Slug: requestErrInvalidFormat, |
| paddy@99 | 265 Field: "/email", |
| paddy@99 | 266 }) |
| paddy@99 | 267 } |
| paddy@99 | 268 return errors |
| paddy@99 | 269 } |
| paddy@99 | 270 |
| paddy@57 | 271 type profileStore interface { |
| paddy@57 | 272 getProfileByID(id uuid.ID) (Profile, error) |
| paddy@69 | 273 getProfileByLogin(value string) (Profile, error) |
| paddy@57 | 274 saveProfile(profile Profile) error |
| paddy@57 | 275 updateProfile(id uuid.ID, change ProfileChange) error |
| paddy@57 | 276 updateProfiles(ids []uuid.ID, change BulkProfileChange) error |
| paddy@44 | 277 |
| paddy@57 | 278 addLogin(login Login) error |
| paddy@69 | 279 removeLogin(value string, profile uuid.ID) error |
| paddy@69 | 280 recordLoginUse(value string, when time.Time) error |
| paddy@57 | 281 listLogins(profile uuid.ID, num, offset int) ([]Login, error) |
| paddy@38 | 282 } |
| paddy@27 | 283 |
| paddy@57 | 284 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) { |
| paddy@38 | 285 m.profileLock.RLock() |
| paddy@38 | 286 defer m.profileLock.RUnlock() |
| paddy@38 | 287 p, ok := m.profiles[id.String()] |
| paddy@38 | 288 if !ok { |
| paddy@38 | 289 return Profile{}, ErrProfileNotFound |
| paddy@38 | 290 } |
| paddy@148 | 291 if p.Deleted { |
| paddy@148 | 292 return Profile{}, ErrProfileNotFound |
| paddy@148 | 293 } |
| paddy@38 | 294 return p, nil |
| paddy@27 | 295 } |
| paddy@38 | 296 |
| paddy@69 | 297 func (m *memstore) getProfileByLogin(value string) (Profile, error) { |
| paddy@44 | 298 m.loginLock.RLock() |
| paddy@44 | 299 defer m.loginLock.RUnlock() |
| paddy@69 | 300 login, ok := m.logins[value] |
| paddy@44 | 301 if !ok { |
| paddy@44 | 302 return Profile{}, ErrLoginNotFound |
| paddy@44 | 303 } |
| paddy@44 | 304 m.profileLock.RLock() |
| paddy@44 | 305 defer m.profileLock.RUnlock() |
| paddy@44 | 306 profile, ok := m.profiles[login.ProfileID.String()] |
| paddy@44 | 307 if !ok { |
| paddy@44 | 308 return Profile{}, ErrProfileNotFound |
| paddy@44 | 309 } |
| paddy@148 | 310 if profile.Deleted { |
| paddy@148 | 311 return Profile{}, ErrProfileNotFound |
| paddy@148 | 312 } |
| paddy@44 | 313 return profile, nil |
| paddy@38 | 314 } |
| paddy@38 | 315 |
| paddy@57 | 316 func (m *memstore) saveProfile(profile Profile) error { |
| paddy@38 | 317 m.profileLock.Lock() |
| paddy@38 | 318 defer m.profileLock.Unlock() |
| paddy@38 | 319 _, ok := m.profiles[profile.ID.String()] |
| paddy@38 | 320 if ok { |
| paddy@38 | 321 return ErrProfileAlreadyExists |
| paddy@38 | 322 } |
| paddy@38 | 323 m.profiles[profile.ID.String()] = profile |
| paddy@38 | 324 return nil |
| paddy@38 | 325 } |
| paddy@38 | 326 |
| paddy@57 | 327 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error { |
| paddy@38 | 328 m.profileLock.Lock() |
| paddy@38 | 329 defer m.profileLock.Unlock() |
| paddy@38 | 330 p, ok := m.profiles[id.String()] |
| paddy@38 | 331 if !ok { |
| paddy@38 | 332 return ErrProfileNotFound |
| paddy@38 | 333 } |
| paddy@38 | 334 p.ApplyChange(change) |
| paddy@38 | 335 m.profiles[id.String()] = p |
| paddy@38 | 336 return nil |
| paddy@38 | 337 } |
| paddy@38 | 338 |
| paddy@57 | 339 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error { |
| paddy@44 | 340 m.profileLock.Lock() |
| paddy@44 | 341 defer m.profileLock.Unlock() |
| paddy@44 | 342 for id, profile := range m.profiles { |
| paddy@44 | 343 for _, i := range ids { |
| paddy@44 | 344 if id == i.String() { |
| paddy@44 | 345 profile.ApplyBulkChange(change) |
| paddy@44 | 346 m.profiles[id] = profile |
| paddy@44 | 347 break |
| paddy@44 | 348 } |
| paddy@44 | 349 } |
| paddy@44 | 350 } |
| paddy@44 | 351 return nil |
| paddy@44 | 352 } |
| paddy@44 | 353 |
| paddy@57 | 354 func (m *memstore) addLogin(login Login) error { |
| paddy@44 | 355 m.loginLock.Lock() |
| paddy@44 | 356 defer m.loginLock.Unlock() |
| paddy@69 | 357 _, ok := m.logins[login.Value] |
| paddy@44 | 358 if ok { |
| paddy@44 | 359 return ErrLoginAlreadyExists |
| paddy@44 | 360 } |
| paddy@69 | 361 m.logins[login.Value] = login |
| paddy@69 | 362 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value) |
| paddy@44 | 363 return nil |
| paddy@44 | 364 } |
| paddy@44 | 365 |
| paddy@69 | 366 func (m *memstore) removeLogin(value string, profile uuid.ID) error { |
| paddy@44 | 367 m.loginLock.Lock() |
| paddy@44 | 368 defer m.loginLock.Unlock() |
| paddy@69 | 369 l, ok := m.logins[value] |
| paddy@44 | 370 if !ok { |
| paddy@44 | 371 return ErrLoginNotFound |
| paddy@44 | 372 } |
| paddy@44 | 373 if !l.ProfileID.Equal(profile) { |
| paddy@44 | 374 return ErrLoginNotFound |
| paddy@44 | 375 } |
| paddy@69 | 376 delete(m.logins, value) |
| paddy@44 | 377 pos := -1 |
| paddy@44 | 378 for p, id := range m.profileLoginLookup[profile.String()] { |
| paddy@69 | 379 if id == value { |
| paddy@44 | 380 pos = p |
| paddy@44 | 381 break |
| paddy@44 | 382 } |
| paddy@44 | 383 } |
| paddy@44 | 384 if pos >= 0 { |
| paddy@44 | 385 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...) |
| paddy@44 | 386 } |
| paddy@44 | 387 return nil |
| paddy@44 | 388 } |
| paddy@44 | 389 |
| paddy@69 | 390 func (m *memstore) recordLoginUse(value string, when time.Time) error { |
| paddy@44 | 391 m.loginLock.Lock() |
| paddy@44 | 392 defer m.loginLock.Unlock() |
| paddy@69 | 393 l, ok := m.logins[value] |
| paddy@44 | 394 if !ok { |
| paddy@44 | 395 return ErrLoginNotFound |
| paddy@44 | 396 } |
| paddy@44 | 397 l.LastUsed = when |
| paddy@69 | 398 m.logins[value] = l |
| paddy@44 | 399 return nil |
| paddy@44 | 400 } |
| paddy@44 | 401 |
| paddy@57 | 402 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) { |
| paddy@44 | 403 m.loginLock.RLock() |
| paddy@44 | 404 defer m.loginLock.RUnlock() |
| paddy@44 | 405 ids, ok := m.profileLoginLookup[profile.String()] |
| paddy@44 | 406 if !ok { |
| paddy@44 | 407 return []Login{}, nil |
| paddy@44 | 408 } |
| paddy@44 | 409 if len(ids) > num+offset { |
| paddy@44 | 410 ids = ids[offset : num+offset] |
| paddy@44 | 411 } else if len(ids) > offset { |
| paddy@44 | 412 ids = ids[offset:] |
| paddy@44 | 413 } else { |
| paddy@44 | 414 return []Login{}, nil |
| paddy@44 | 415 } |
| paddy@44 | 416 logins := []Login{} |
| paddy@44 | 417 for _, id := range ids { |
| paddy@44 | 418 login, ok := m.logins[id] |
| paddy@44 | 419 if !ok { |
| paddy@44 | 420 continue |
| paddy@44 | 421 } |
| paddy@44 | 422 logins = append(logins, login) |
| paddy@44 | 423 } |
| paddy@44 | 424 return logins, nil |
| paddy@44 | 425 } |
| paddy@99 | 426 |
| paddy@105 | 427 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval. |
| paddy@105 | 428 func RegisterProfileHandlers(r *mux.Router, context Context) { |
| paddy@105 | 429 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST") |
| paddy@128 | 430 // BUG(paddy): We need to implement a handler that will return information about a profile or set of profiles. |
| paddy@145 | 431 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH") |
| paddy@128 | 432 // 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 | 433 // BUG(paddy): We need to implement a handler that will add a login to a profile. |
| paddy@128 | 434 // 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 | 435 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile. |
| paddy@105 | 436 } |
| paddy@105 | 437 |
| paddy@99 | 438 // CreateProfileHandler is an HTTP handler for registering new profiles. |
| paddy@99 | 439 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@99 | 440 scheme, ok := passphraseSchemes[CurPassphraseScheme] |
| paddy@99 | 441 if !ok { |
| paddy@149 | 442 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme) |
| paddy@105 | 443 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 444 return |
| paddy@99 | 445 } |
| paddy@99 | 446 var req newProfileRequest |
| paddy@99 | 447 errors := []requestError{} |
| paddy@99 | 448 decoder := json.NewDecoder(r.Body) |
| paddy@99 | 449 err := decoder.Decode(&req) |
| paddy@99 | 450 if err != nil { |
| paddy@105 | 451 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@99 | 452 return |
| paddy@99 | 453 } |
| paddy@99 | 454 errors = append(errors, validateNewProfileRequest(&req)...) |
| paddy@99 | 455 if len(errors) > 0 { |
| paddy@105 | 456 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@99 | 457 return |
| paddy@99 | 458 } |
| paddy@99 | 459 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations) |
| paddy@99 | 460 if err != nil { |
| paddy@149 | 461 log.Printf("Error creating encoded passphrase: %#+v\n", err) |
| paddy@105 | 462 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 463 return |
| paddy@99 | 464 } |
| paddy@99 | 465 profile := Profile{ |
| paddy@99 | 466 ID: uuid.NewID(), |
| paddy@99 | 467 Name: req.Name, |
| paddy@99 | 468 Passphrase: string(passphrase), |
| paddy@99 | 469 Iterations: context.config.iterations, |
| paddy@99 | 470 Salt: string(salt), |
| paddy@99 | 471 PassphraseScheme: CurPassphraseScheme, |
| paddy@99 | 472 Created: time.Now(), |
| paddy@99 | 473 LastSeen: time.Now(), |
| paddy@99 | 474 } |
| paddy@99 | 475 err = context.SaveProfile(profile) |
| paddy@99 | 476 if err != nil { |
| paddy@105 | 477 if err == ErrProfileAlreadyExists { |
| paddy@105 | 478 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}}) |
| paddy@105 | 479 return |
| paddy@105 | 480 } |
| paddy@149 | 481 log.Printf("Error saving profile: %#+v\n", err) |
| paddy@105 | 482 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 483 return |
| paddy@99 | 484 } |
| paddy@99 | 485 logins := []Login{} |
| paddy@99 | 486 login := Login{ |
| paddy@99 | 487 Type: "email", |
| paddy@99 | 488 Value: req.Email, |
| paddy@99 | 489 Created: profile.Created, |
| paddy@99 | 490 LastUsed: profile.Created, |
| paddy@99 | 491 ProfileID: profile.ID, |
| paddy@99 | 492 } |
| paddy@99 | 493 err = context.AddLogin(login) |
| paddy@99 | 494 if err != nil { |
| paddy@105 | 495 if err == ErrLoginAlreadyExists { |
| paddy@105 | 496 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}}) |
| paddy@105 | 497 return |
| paddy@105 | 498 } |
| paddy@149 | 499 log.Printf("Error adding login: %#+v\n", err) |
| paddy@105 | 500 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 501 return |
| paddy@99 | 502 } |
| paddy@99 | 503 logins = append(logins, login) |
| paddy@99 | 504 if req.Username != "" { |
| paddy@99 | 505 login.Type = "username" |
| paddy@99 | 506 login.Value = req.Username |
| paddy@99 | 507 err = context.AddLogin(login) |
| paddy@99 | 508 if err != nil { |
| paddy@105 | 509 if err == ErrLoginAlreadyExists { |
| paddy@105 | 510 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/username"}}}) |
| paddy@105 | 511 return |
| paddy@105 | 512 } |
| paddy@149 | 513 log.Printf("Error adding login: %#+v\n", err) |
| paddy@105 | 514 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 515 return |
| paddy@99 | 516 } |
| paddy@99 | 517 logins = append(logins, login) |
| paddy@99 | 518 } |
| paddy@105 | 519 resp := response{ |
| paddy@105 | 520 Logins: logins, |
| paddy@105 | 521 Profiles: []Profile{profile}, |
| paddy@105 | 522 } |
| paddy@105 | 523 encode(w, r, http.StatusCreated, resp) |
| paddy@99 | 524 // TODO(paddy): should we kick off the email validation flow? |
| paddy@99 | 525 } |
| paddy@145 | 526 |
| paddy@145 | 527 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@145 | 528 errors := []requestError{} |
| paddy@145 | 529 vars := mux.Vars(r) |
| paddy@145 | 530 if vars["id"] == "" { |
| paddy@145 | 531 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@145 | 532 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@145 | 533 return |
| paddy@145 | 534 } |
| paddy@145 | 535 id, err := uuid.Parse(vars["id"]) |
| paddy@145 | 536 if err != nil { |
| paddy@145 | 537 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@145 | 538 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@145 | 539 return |
| paddy@145 | 540 } |
| paddy@145 | 541 username, password, ok := r.BasicAuth() |
| paddy@145 | 542 if !ok { |
| paddy@145 | 543 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@145 | 544 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@145 | 545 return |
| paddy@145 | 546 } |
| paddy@145 | 547 profile, err := authenticate(username, password, context) |
| paddy@145 | 548 if err != nil { |
| paddy@145 | 549 if isAuthError(err) { |
| paddy@145 | 550 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@145 | 551 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@145 | 552 } else { |
| paddy@145 | 553 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@145 | 554 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@145 | 555 } |
| paddy@145 | 556 return |
| paddy@145 | 557 } |
| paddy@145 | 558 if !profile.ID.Equal(id) { |
| paddy@145 | 559 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@145 | 560 encode(w, r, http.StatusForbidden, response{Errors: errors}) |
| paddy@145 | 561 return |
| paddy@145 | 562 } |
| paddy@145 | 563 var req ProfileChange |
| paddy@145 | 564 decoder := json.NewDecoder(r.Body) |
| paddy@145 | 565 err = decoder.Decode(&req) |
| paddy@145 | 566 if err != nil { |
| paddy@149 | 567 log.Printf("Error decoding request: %#+v\n", err) |
| paddy@145 | 568 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@145 | 569 return |
| paddy@145 | 570 } |
| paddy@145 | 571 req.Iterations = nil |
| paddy@145 | 572 req.Salt = nil |
| paddy@145 | 573 req.PassphraseScheme = nil |
| paddy@145 | 574 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised |
| paddy@145 | 575 req.LockedUntil = nil |
| paddy@145 | 576 req.LastSeen = nil |
| paddy@145 | 577 if req.Passphrase != nil { |
| paddy@145 | 578 if len(*req.Passphrase) < MinPassphraseLength { |
| paddy@145 | 579 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/passphrase"}) |
| paddy@145 | 580 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@145 | 581 return |
| paddy@145 | 582 } |
| paddy@145 | 583 if len(*req.Passphrase) > MaxPassphraseLength { |
| paddy@145 | 584 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/passphrase"}) |
| paddy@145 | 585 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@145 | 586 return |
| paddy@145 | 587 } |
| paddy@145 | 588 iterations := context.config.iterations |
| paddy@145 | 589 scheme, ok := passphraseSchemes[CurPassphraseScheme] |
| paddy@145 | 590 if !ok { |
| paddy@145 | 591 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@145 | 592 return |
| paddy@145 | 593 } |
| paddy@145 | 594 curScheme := CurPassphraseScheme |
| paddy@145 | 595 req.PassphraseScheme = &curScheme |
| paddy@145 | 596 passphrase, salt, err := scheme.create(*req.Passphrase, iterations) |
| paddy@145 | 597 if err != nil { |
| paddy@145 | 598 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@145 | 599 return |
| paddy@145 | 600 } |
| paddy@145 | 601 req.Passphrase = &passphrase |
| paddy@145 | 602 req.Salt = &salt |
| paddy@145 | 603 req.Iterations = &iterations |
| paddy@145 | 604 } |
| paddy@145 | 605 if req.PassphraseReset != nil { |
| paddy@145 | 606 now := time.Now() |
| paddy@145 | 607 req.PassphraseResetCreated = &now |
| paddy@145 | 608 } |
| paddy@145 | 609 err = req.Validate() |
| paddy@145 | 610 if err != nil { |
| paddy@145 | 611 var status int |
| paddy@145 | 612 var resp response |
| paddy@145 | 613 switch err { |
| paddy@145 | 614 case ErrEmptyChange: |
| paddy@145 | 615 resp.Profiles = []Profile{profile} |
| paddy@145 | 616 status = http.StatusOK |
| paddy@145 | 617 default: |
| paddy@145 | 618 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@145 | 619 resp.Errors = errors |
| paddy@145 | 620 status = http.StatusInternalServerError |
| paddy@145 | 621 } |
| paddy@145 | 622 encode(w, r, status, resp) |
| paddy@145 | 623 return |
| paddy@145 | 624 } |
| paddy@145 | 625 err = context.UpdateProfile(id, req) |
| paddy@145 | 626 if err != nil { |
| paddy@145 | 627 if err == ErrProfileNotFound { |
| paddy@145 | 628 errors = append(errors, requestError{Slug: requestErrNotFound}) |
| paddy@145 | 629 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@145 | 630 return |
| paddy@145 | 631 } |
| paddy@145 | 632 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@145 | 633 return |
| paddy@145 | 634 } |
| paddy@145 | 635 profile.ApplyChange(req) |
| paddy@145 | 636 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}}) |
| paddy@145 | 637 return |
| paddy@145 | 638 } |