auth

Paddy 2015-03-20 Parent:e660a38fa936 Child:06fb735031bb

147:7ae03163f578 Go to Latest

auth/profile.go

Randomly generate codes. We've been using our IDs for auth codes. But our IDs may at some point be non-random, for the purpose of optimising database performance, or some other perfectly valid reason. Auth codes we always want to be random, and have no relation to IDs, so why conflate them? Instead, we pull 16 random bytes out of crypto/rand.Reader and hex encode them.

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