auth

Paddy 2015-03-21 Parent:e660a38fa936 Child:8267e1c8bcd1

148:06fb735031bb Go to Latest

auth/profile.go

Do a first, naive pass at storing profiles in Postgres. This is untested against an actual database. It's a best-guess attempt at SQL. It _should_ work. I think. Start storing things in Postgres, starting with Profiles and Logins. This necessitates the addition of a Deleted property to the Profile type, because I'm not deleting those in case of accidental deletion. Logins, though, we'll delete. This also necessitates updating the profileStore interface to no longer have a deleteProfile method, because we're tracking that through updates now. Then we need to update our profileStore tests, because they no longer clean up after themselves. Which, come to think of it, may cause some problems later.

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