auth

Paddy 2015-04-11 Parent:8267e1c8bcd1 Child:3223a8e679db

157:202e991accc2 Go to Latest

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.

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@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 }