auth

Paddy 2015-04-11 Parent:8267e1c8bcd1 Child:48200d8c4036

158:3223a8e679db Go to Latest

auth/profile.go

Remove concept of usernames. We really have no reason to use usernames, and they're complicating things more than they need to. We're going to keep logins the same, because we want to be able to support OAuth2/OpenID/whatever logins in the future, and keeping a type associated with those logins is probably for the best.

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