auth

Paddy 2015-04-11 Parent:3223a8e679db Child:849f3820b164

160:48200d8c4036 Go to Latest

auth/profile.go

Start to support deleting profiles through the API. Create a removeLoginsByProfile method on the profileStore, to allow an easy way to bulk-delete logins associated with a Profile after the Profile has been deleted. Create postgres and memstore implementations of the removeLoginsByProfile method. Create a cleanUpAfterProfileDeletion helper method that will clean up the child objects of a Profile (its Sessions, Tokens, Clients, etc.). The intended usage is to call this in a goroutine after a Profile has been deleted, to try and get things back in order. Detect when the UpdateProfileHandler API is used to set the Deleted flag of a Profile to true, and clean up after the Profile when that's the case. Add a DeleteProfileHandler API endpoint that is a shortcut to setting the Deleted flag of a Profile to true and cleaning up after the Profile. The problem with our approach thus far is that some of it is reversible and some is not. If a Profile is maliciously/accidentally deleted, it's simple enough to use the API as a superuser to restore the Profile. But doing that will not (and cannot) restore the Logins associated with that Profile, for example. While it would be nice to add a Deleted flag to our Logins that we could simply toggle, that would wreak havoc with our database constraints and ensuring uniqueness of Login values. I still don't have a solution for this, outside the superuser manually restoring a Login for the Profile, after which the user can authenticate themselves and add more Logins as desired. But there has to be a better way. I suppose since the passphrase is being stored with the Profile and not the Login, we could offer an endpoint that would automate this, but... well, that would be tricky. It would require the user remembering their Profile ID, and let's be honest, nobody's going to remember a UUID. Maybe such an endpoint would help from a customer service standpoint: we identify their Profile manually, then send them to /profiles/ID/restorelogin or something, and that lets them add a Login back to the Profile. I'll figure it out later. For now, we know we at least have enough information to identify a user is who they say they are and resolve the situation manually.

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@160 268 removeLoginsByProfile(profile uuid.ID) error
paddy@69 269 recordLoginUse(value string, when time.Time) error
paddy@57 270 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
paddy@38 271 }
paddy@27 272
paddy@57 273 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
paddy@38 274 m.profileLock.RLock()
paddy@38 275 defer m.profileLock.RUnlock()
paddy@38 276 p, ok := m.profiles[id.String()]
paddy@38 277 if !ok {
paddy@38 278 return Profile{}, ErrProfileNotFound
paddy@38 279 }
paddy@148 280 if p.Deleted {
paddy@148 281 return Profile{}, ErrProfileNotFound
paddy@148 282 }
paddy@38 283 return p, nil
paddy@27 284 }
paddy@38 285
paddy@69 286 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
paddy@44 287 m.loginLock.RLock()
paddy@44 288 defer m.loginLock.RUnlock()
paddy@69 289 login, ok := m.logins[value]
paddy@44 290 if !ok {
paddy@44 291 return Profile{}, ErrLoginNotFound
paddy@44 292 }
paddy@44 293 m.profileLock.RLock()
paddy@44 294 defer m.profileLock.RUnlock()
paddy@44 295 profile, ok := m.profiles[login.ProfileID.String()]
paddy@44 296 if !ok {
paddy@44 297 return Profile{}, ErrProfileNotFound
paddy@44 298 }
paddy@148 299 if profile.Deleted {
paddy@148 300 return Profile{}, ErrProfileNotFound
paddy@148 301 }
paddy@44 302 return profile, nil
paddy@38 303 }
paddy@38 304
paddy@57 305 func (m *memstore) saveProfile(profile Profile) error {
paddy@38 306 m.profileLock.Lock()
paddy@38 307 defer m.profileLock.Unlock()
paddy@38 308 _, ok := m.profiles[profile.ID.String()]
paddy@38 309 if ok {
paddy@38 310 return ErrProfileAlreadyExists
paddy@38 311 }
paddy@38 312 m.profiles[profile.ID.String()] = profile
paddy@38 313 return nil
paddy@38 314 }
paddy@38 315
paddy@57 316 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
paddy@38 317 m.profileLock.Lock()
paddy@38 318 defer m.profileLock.Unlock()
paddy@38 319 p, ok := m.profiles[id.String()]
paddy@38 320 if !ok {
paddy@38 321 return ErrProfileNotFound
paddy@38 322 }
paddy@38 323 p.ApplyChange(change)
paddy@38 324 m.profiles[id.String()] = p
paddy@38 325 return nil
paddy@38 326 }
paddy@38 327
paddy@57 328 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
paddy@44 329 m.profileLock.Lock()
paddy@44 330 defer m.profileLock.Unlock()
paddy@44 331 for id, profile := range m.profiles {
paddy@44 332 for _, i := range ids {
paddy@44 333 if id == i.String() {
paddy@44 334 profile.ApplyBulkChange(change)
paddy@44 335 m.profiles[id] = profile
paddy@44 336 break
paddy@44 337 }
paddy@44 338 }
paddy@44 339 }
paddy@44 340 return nil
paddy@44 341 }
paddy@44 342
paddy@57 343 func (m *memstore) addLogin(login Login) error {
paddy@44 344 m.loginLock.Lock()
paddy@44 345 defer m.loginLock.Unlock()
paddy@69 346 _, ok := m.logins[login.Value]
paddy@44 347 if ok {
paddy@44 348 return ErrLoginAlreadyExists
paddy@44 349 }
paddy@69 350 m.logins[login.Value] = login
paddy@69 351 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
paddy@44 352 return nil
paddy@44 353 }
paddy@44 354
paddy@69 355 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
paddy@44 356 m.loginLock.Lock()
paddy@44 357 defer m.loginLock.Unlock()
paddy@69 358 l, ok := m.logins[value]
paddy@44 359 if !ok {
paddy@44 360 return ErrLoginNotFound
paddy@44 361 }
paddy@44 362 if !l.ProfileID.Equal(profile) {
paddy@44 363 return ErrLoginNotFound
paddy@44 364 }
paddy@69 365 delete(m.logins, value)
paddy@44 366 pos := -1
paddy@44 367 for p, id := range m.profileLoginLookup[profile.String()] {
paddy@69 368 if id == value {
paddy@44 369 pos = p
paddy@44 370 break
paddy@44 371 }
paddy@44 372 }
paddy@44 373 if pos >= 0 {
paddy@44 374 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
paddy@44 375 }
paddy@44 376 return nil
paddy@44 377 }
paddy@44 378
paddy@160 379 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
paddy@160 380 m.loginLock.Lock()
paddy@160 381 defer m.loginLock.Unlock()
paddy@160 382 logins, ok := m.profileLoginLookup[profile.String()]
paddy@160 383 if !ok {
paddy@160 384 return ErrProfileNotFound
paddy@160 385 }
paddy@160 386 delete(m.profileLoginLookup, profile.String())
paddy@160 387 for _, login := range logins {
paddy@160 388 delete(m.logins, login)
paddy@160 389 }
paddy@160 390 return nil
paddy@160 391 }
paddy@160 392
paddy@69 393 func (m *memstore) recordLoginUse(value string, when time.Time) error {
paddy@44 394 m.loginLock.Lock()
paddy@44 395 defer m.loginLock.Unlock()
paddy@69 396 l, ok := m.logins[value]
paddy@44 397 if !ok {
paddy@44 398 return ErrLoginNotFound
paddy@44 399 }
paddy@44 400 l.LastUsed = when
paddy@69 401 m.logins[value] = l
paddy@44 402 return nil
paddy@44 403 }
paddy@44 404
paddy@57 405 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
paddy@44 406 m.loginLock.RLock()
paddy@44 407 defer m.loginLock.RUnlock()
paddy@44 408 ids, ok := m.profileLoginLookup[profile.String()]
paddy@44 409 if !ok {
paddy@44 410 return []Login{}, nil
paddy@44 411 }
paddy@44 412 if len(ids) > num+offset {
paddy@44 413 ids = ids[offset : num+offset]
paddy@44 414 } else if len(ids) > offset {
paddy@44 415 ids = ids[offset:]
paddy@44 416 } else {
paddy@44 417 return []Login{}, nil
paddy@44 418 }
paddy@44 419 logins := []Login{}
paddy@44 420 for _, id := range ids {
paddy@44 421 login, ok := m.logins[id]
paddy@44 422 if !ok {
paddy@44 423 continue
paddy@44 424 }
paddy@44 425 logins = append(logins, login)
paddy@44 426 }
paddy@44 427 return logins, nil
paddy@44 428 }
paddy@99 429
paddy@160 430 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
paddy@160 431 err := context.RemoveLoginsByProfile(profile)
paddy@160 432 if err != nil {
paddy@160 433 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
paddy@160 434 }
paddy@160 435 // BUG(paddy): need to terminate all sessions associated with the Profile
paddy@160 436 // BUG(paddy): need to invalidate all tokens associated with the Profile
paddy@160 437 // BUG(paddy): need to delete all the grants associated with the Profile
paddy@160 438 }
paddy@160 439
paddy@105 440 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
paddy@105 441 func RegisterProfileHandlers(r *mux.Router, context Context) {
paddy@105 442 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST")
paddy@128 443 // BUG(paddy): We need to implement a handler that will return information about a profile or set of profiles.
paddy@145 444 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH")
paddy@160 445 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE")
paddy@128 446 // BUG(paddy): We need to implement a handler that will add a login to a profile.
paddy@128 447 // 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 448 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
paddy@105 449 }
paddy@105 450
paddy@99 451 // CreateProfileHandler is an HTTP handler for registering new profiles.
paddy@99 452 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@99 453 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@99 454 if !ok {
paddy@149 455 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
paddy@105 456 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 457 return
paddy@99 458 }
paddy@99 459 var req newProfileRequest
paddy@99 460 errors := []requestError{}
paddy@99 461 decoder := json.NewDecoder(r.Body)
paddy@99 462 err := decoder.Decode(&req)
paddy@99 463 if err != nil {
paddy@105 464 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@99 465 return
paddy@99 466 }
paddy@99 467 errors = append(errors, validateNewProfileRequest(&req)...)
paddy@99 468 if len(errors) > 0 {
paddy@105 469 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@99 470 return
paddy@99 471 }
paddy@99 472 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
paddy@99 473 if err != nil {
paddy@149 474 log.Printf("Error creating encoded passphrase: %#+v\n", err)
paddy@105 475 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 476 return
paddy@99 477 }
paddy@99 478 profile := Profile{
paddy@99 479 ID: uuid.NewID(),
paddy@99 480 Name: req.Name,
paddy@99 481 Passphrase: string(passphrase),
paddy@99 482 Iterations: context.config.iterations,
paddy@99 483 Salt: string(salt),
paddy@99 484 PassphraseScheme: CurPassphraseScheme,
paddy@99 485 Created: time.Now(),
paddy@99 486 LastSeen: time.Now(),
paddy@99 487 }
paddy@99 488 err = context.SaveProfile(profile)
paddy@99 489 if err != nil {
paddy@105 490 if err == ErrProfileAlreadyExists {
paddy@105 491 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}})
paddy@105 492 return
paddy@105 493 }
paddy@149 494 log.Printf("Error saving profile: %#+v\n", err)
paddy@105 495 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 496 return
paddy@99 497 }
paddy@99 498 logins := []Login{}
paddy@99 499 login := Login{
paddy@99 500 Type: "email",
paddy@99 501 Value: req.Email,
paddy@99 502 Created: profile.Created,
paddy@99 503 LastUsed: profile.Created,
paddy@99 504 ProfileID: profile.ID,
paddy@99 505 }
paddy@99 506 err = context.AddLogin(login)
paddy@99 507 if err != nil {
paddy@105 508 if err == ErrLoginAlreadyExists {
paddy@105 509 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}})
paddy@105 510 return
paddy@105 511 }
paddy@149 512 log.Printf("Error adding login: %#+v\n", err)
paddy@105 513 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 514 return
paddy@99 515 }
paddy@99 516 logins = append(logins, login)
paddy@105 517 resp := response{
paddy@105 518 Logins: logins,
paddy@105 519 Profiles: []Profile{profile},
paddy@105 520 }
paddy@105 521 encode(w, r, http.StatusCreated, resp)
paddy@99 522 // TODO(paddy): should we kick off the email validation flow?
paddy@99 523 }
paddy@145 524
paddy@145 525 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@145 526 errors := []requestError{}
paddy@145 527 vars := mux.Vars(r)
paddy@145 528 if vars["id"] == "" {
paddy@145 529 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@145 530 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@145 531 return
paddy@145 532 }
paddy@145 533 id, err := uuid.Parse(vars["id"])
paddy@145 534 if err != nil {
paddy@145 535 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@145 536 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@145 537 return
paddy@145 538 }
paddy@145 539 username, password, ok := r.BasicAuth()
paddy@145 540 if !ok {
paddy@145 541 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@145 542 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@145 543 return
paddy@145 544 }
paddy@145 545 profile, err := authenticate(username, password, context)
paddy@145 546 if err != nil {
paddy@145 547 if isAuthError(err) {
paddy@145 548 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@145 549 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@145 550 } else {
paddy@145 551 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@145 552 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@145 553 }
paddy@145 554 return
paddy@145 555 }
paddy@145 556 if !profile.ID.Equal(id) {
paddy@145 557 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@145 558 encode(w, r, http.StatusForbidden, response{Errors: errors})
paddy@145 559 return
paddy@145 560 }
paddy@145 561 var req ProfileChange
paddy@145 562 decoder := json.NewDecoder(r.Body)
paddy@145 563 err = decoder.Decode(&req)
paddy@145 564 if err != nil {
paddy@149 565 log.Printf("Error decoding request: %#+v\n", err)
paddy@145 566 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@145 567 return
paddy@145 568 }
paddy@145 569 req.Iterations = nil
paddy@145 570 req.Salt = nil
paddy@145 571 req.PassphraseScheme = nil
paddy@145 572 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
paddy@145 573 req.LockedUntil = nil
paddy@145 574 req.LastSeen = nil
paddy@145 575 if req.Passphrase != nil {
paddy@145 576 if len(*req.Passphrase) < MinPassphraseLength {
paddy@145 577 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/passphrase"})
paddy@145 578 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@145 579 return
paddy@145 580 }
paddy@145 581 if len(*req.Passphrase) > MaxPassphraseLength {
paddy@145 582 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/passphrase"})
paddy@145 583 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@145 584 return
paddy@145 585 }
paddy@145 586 iterations := context.config.iterations
paddy@145 587 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@145 588 if !ok {
paddy@145 589 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 590 return
paddy@145 591 }
paddy@145 592 curScheme := CurPassphraseScheme
paddy@145 593 req.PassphraseScheme = &curScheme
paddy@145 594 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
paddy@145 595 if err != nil {
paddy@145 596 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 597 return
paddy@145 598 }
paddy@145 599 req.Passphrase = &passphrase
paddy@145 600 req.Salt = &salt
paddy@145 601 req.Iterations = &iterations
paddy@145 602 }
paddy@145 603 if req.PassphraseReset != nil {
paddy@145 604 now := time.Now()
paddy@145 605 req.PassphraseResetCreated = &now
paddy@145 606 }
paddy@145 607 err = req.Validate()
paddy@145 608 if err != nil {
paddy@145 609 var status int
paddy@145 610 var resp response
paddy@145 611 switch err {
paddy@145 612 case ErrEmptyChange:
paddy@145 613 resp.Profiles = []Profile{profile}
paddy@145 614 status = http.StatusOK
paddy@145 615 default:
paddy@145 616 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@145 617 resp.Errors = errors
paddy@145 618 status = http.StatusInternalServerError
paddy@145 619 }
paddy@145 620 encode(w, r, status, resp)
paddy@145 621 return
paddy@145 622 }
paddy@145 623 err = context.UpdateProfile(id, req)
paddy@145 624 if err != nil {
paddy@145 625 if err == ErrProfileNotFound {
paddy@145 626 errors = append(errors, requestError{Slug: requestErrNotFound})
paddy@145 627 encode(w, r, http.StatusNotFound, response{Errors: errors})
paddy@145 628 return
paddy@145 629 }
paddy@145 630 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 631 return
paddy@145 632 }
paddy@160 633 if !profile.Deleted && req.Deleted != nil && *req.Deleted {
paddy@160 634 go cleanUpAfterProfileDeletion(profile.ID, context)
paddy@160 635 }
paddy@145 636 profile.ApplyChange(req)
paddy@145 637 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
paddy@145 638 return
paddy@145 639 }
paddy@160 640
paddy@160 641 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@160 642 errors := []requestError{}
paddy@160 643 vars := mux.Vars(r)
paddy@160 644 if vars["id"] == "" {
paddy@160 645 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@160 646 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@160 647 return
paddy@160 648 }
paddy@160 649 id, err := uuid.Parse(vars["id"])
paddy@160 650 if err != nil {
paddy@160 651 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@160 652 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@160 653 return
paddy@160 654 }
paddy@160 655 username, password, ok := r.BasicAuth()
paddy@160 656 if !ok {
paddy@160 657 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@160 658 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@160 659 return
paddy@160 660 }
paddy@160 661 profile, err := authenticate(username, password, context)
paddy@160 662 if err != nil {
paddy@160 663 if isAuthError(err) {
paddy@160 664 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@160 665 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@160 666 } else {
paddy@160 667 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@160 668 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@160 669 }
paddy@160 670 return
paddy@160 671 }
paddy@160 672 if !profile.ID.Equal(id) {
paddy@160 673 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@160 674 encode(w, r, http.StatusForbidden, response{Errors: errors})
paddy@160 675 return
paddy@160 676 }
paddy@160 677 var change ProfileChange
paddy@160 678 deleted := true
paddy@160 679 change.Deleted = &deleted
paddy@160 680 err = context.UpdateProfile(id, change)
paddy@160 681 if err != nil {
paddy@160 682 if err == ErrProfileNotFound {
paddy@160 683 errors = append(errors, requestError{Slug: requestErrNotFound})
paddy@160 684 encode(w, r, http.StatusNotFound, response{Errors: errors})
paddy@160 685 return
paddy@160 686 }
paddy@160 687 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@160 688 return
paddy@160 689 }
paddy@160 690 profile.ApplyChange(change)
paddy@160 691 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
paddy@160 692 go cleanUpAfterProfileDeletion(profile.ID, context)
paddy@160 693 }