auth

Paddy 2015-07-13 Parent:b0d1b3e39fc8 Child:0a2c3d677161

177:5d52b9d83184 Go to Latest

auth/profile.go

Add notes about model events. We need to sensibly trigger events about models, so I left some basic notes about triggering an event when a Profile is created.

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@172 42 // ErrLoginVerificationInvalid is returned when a Login is verified with the wrong verification code.
paddy@172 43 ErrLoginVerificationInvalid = errors.New("login verification code incorrect")
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@173 67
paddy@173 68 ScopeLoginAdmin = Scope{ID: "login_admin", Name: "Administer Logins", Description: "Read and write logins, bypassing ACL."}
paddy@38 69 )
paddy@38 70
paddy@57 71 // Profile represents a single user of the service,
paddy@158 72 // including their authentication information.
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@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@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@38 143 }
paddy@38 144
paddy@149 145 func (c ProfileChange) Empty() bool {
paddy@161 146 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)
paddy@149 147 }
paddy@149 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@149 154 if c.Empty() {
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@149 182 func (b BulkProfileChange) Empty() bool {
paddy@149 183 return b.Compromised == nil
paddy@149 184 }
paddy@149 185
paddy@57 186 // Validate checks the BulkProfileChange it is called on
paddy@57 187 // and asserts its internal validity, or lack thereof.
paddy@57 188 // A descriptive error will be returned in the case of an
paddy@57 189 // invalid change.
paddy@44 190 func (b BulkProfileChange) Validate() error {
paddy@149 191 if b.Empty() {
paddy@48 192 return ErrEmptyChange
paddy@48 193 }
paddy@44 194 return nil
paddy@44 195 }
paddy@44 196
paddy@57 197 // Login represents a single human-friendly identifier for
paddy@57 198 // a given Profile that can be used to log into that Profile.
paddy@57 199 // Each Profile may only have one Login for each Type.
paddy@27 200 type Login struct {
paddy@172 201 Type string `json:"type,omitempty"`
paddy@172 202 Value string `json:"value,omitempty"`
paddy@172 203 ProfileID uuid.ID `json:"profile_id,omitempty"`
paddy@172 204 Created time.Time `json:"created,omitempty"`
paddy@172 205 LastUsed time.Time `json:"last_used,omitempty"`
paddy@172 206 Verification string `json:"-"`
paddy@172 207 Verified bool `json:"verified"`
paddy@172 208 }
paddy@172 209
paddy@172 210 type LoginChange struct {
paddy@172 211 Verification *string `json:"verification,omitempty"`
paddy@172 212 ResendVerification *bool `json:"resend_verification,omitempty"`
paddy@27 213 }
paddy@27 214
paddy@99 215 type newProfileRequest struct {
paddy@99 216 Email string `json:"email"`
paddy@99 217 Passphrase string `json:"passphrase"`
paddy@99 218 Name string `json:"name"`
paddy@99 219 }
paddy@99 220
paddy@172 221 func validateNewProfileRequest(req *newProfileRequest) []RequestError {
paddy@172 222 errors := []RequestError{}
paddy@99 223 req.Name = strings.TrimSpace(req.Name)
paddy@99 224 req.Email = strings.TrimSpace(req.Email)
paddy@99 225 if len(req.Passphrase) < MinPassphraseLength {
paddy@172 226 errors = append(errors, RequestError{
paddy@172 227 Slug: RequestErrInsufficient,
paddy@99 228 Field: "/passphrase",
paddy@99 229 })
paddy@99 230 }
paddy@99 231 if len(req.Passphrase) > MaxPassphraseLength {
paddy@172 232 errors = append(errors, RequestError{
paddy@172 233 Slug: RequestErrOverflow,
paddy@99 234 Field: "/passphrase",
paddy@99 235 })
paddy@99 236 }
paddy@99 237 if len(req.Name) > MaxNameLength {
paddy@172 238 errors = append(errors, RequestError{
paddy@172 239 Slug: RequestErrOverflow,
paddy@99 240 Field: "/name",
paddy@99 241 })
paddy@99 242 }
paddy@99 243 if req.Email == "" {
paddy@172 244 errors = append(errors, RequestError{
paddy@172 245 Slug: RequestErrMissing,
paddy@99 246 Field: "/email",
paddy@99 247 })
paddy@99 248 }
paddy@99 249 if len(req.Email) > MaxEmailLength {
paddy@172 250 errors = append(errors, RequestError{
paddy@172 251 Slug: RequestErrOverflow,
paddy@99 252 Field: "/email",
paddy@99 253 })
paddy@99 254 }
paddy@99 255 re := regexp.MustCompile(".+@.+\\..+")
paddy@99 256 if !re.Match([]byte(req.Email)) {
paddy@172 257 errors = append(errors, RequestError{
paddy@172 258 Slug: RequestErrInvalidFormat,
paddy@99 259 Field: "/email",
paddy@99 260 })
paddy@99 261 }
paddy@99 262 return errors
paddy@99 263 }
paddy@99 264
paddy@57 265 type profileStore interface {
paddy@57 266 getProfileByID(id uuid.ID) (Profile, error)
paddy@69 267 getProfileByLogin(value string) (Profile, error)
paddy@57 268 saveProfile(profile Profile) error
paddy@57 269 updateProfile(id uuid.ID, change ProfileChange) error
paddy@57 270 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
paddy@161 271 deleteProfile(id uuid.ID) error
paddy@44 272
paddy@57 273 addLogin(login Login) error
paddy@172 274 getLogin(value string) (Login, error)
paddy@69 275 removeLogin(value string, profile uuid.ID) error
paddy@160 276 removeLoginsByProfile(profile uuid.ID) error
paddy@69 277 recordLoginUse(value string, when time.Time) error
paddy@172 278 verifyLogin(value, verification string) error
paddy@57 279 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
paddy@38 280 }
paddy@27 281
paddy@57 282 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
paddy@38 283 m.profileLock.RLock()
paddy@38 284 defer m.profileLock.RUnlock()
paddy@38 285 p, ok := m.profiles[id.String()]
paddy@38 286 if !ok {
paddy@38 287 return Profile{}, ErrProfileNotFound
paddy@38 288 }
paddy@38 289 return p, nil
paddy@27 290 }
paddy@38 291
paddy@69 292 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
paddy@44 293 m.loginLock.RLock()
paddy@44 294 defer m.loginLock.RUnlock()
paddy@69 295 login, ok := m.logins[value]
paddy@44 296 if !ok {
paddy@44 297 return Profile{}, ErrLoginNotFound
paddy@44 298 }
paddy@44 299 m.profileLock.RLock()
paddy@44 300 defer m.profileLock.RUnlock()
paddy@44 301 profile, ok := m.profiles[login.ProfileID.String()]
paddy@44 302 if !ok {
paddy@44 303 return Profile{}, ErrProfileNotFound
paddy@44 304 }
paddy@44 305 return profile, nil
paddy@38 306 }
paddy@38 307
paddy@57 308 func (m *memstore) saveProfile(profile Profile) error {
paddy@38 309 m.profileLock.Lock()
paddy@38 310 defer m.profileLock.Unlock()
paddy@38 311 _, ok := m.profiles[profile.ID.String()]
paddy@38 312 if ok {
paddy@38 313 return ErrProfileAlreadyExists
paddy@38 314 }
paddy@38 315 m.profiles[profile.ID.String()] = profile
paddy@38 316 return nil
paddy@38 317 }
paddy@38 318
paddy@57 319 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
paddy@38 320 m.profileLock.Lock()
paddy@38 321 defer m.profileLock.Unlock()
paddy@38 322 p, ok := m.profiles[id.String()]
paddy@38 323 if !ok {
paddy@38 324 return ErrProfileNotFound
paddy@38 325 }
paddy@38 326 p.ApplyChange(change)
paddy@38 327 m.profiles[id.String()] = p
paddy@38 328 return nil
paddy@38 329 }
paddy@38 330
paddy@57 331 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
paddy@44 332 m.profileLock.Lock()
paddy@44 333 defer m.profileLock.Unlock()
paddy@44 334 for id, profile := range m.profiles {
paddy@44 335 for _, i := range ids {
paddy@44 336 if id == i.String() {
paddy@44 337 profile.ApplyBulkChange(change)
paddy@44 338 m.profiles[id] = profile
paddy@44 339 break
paddy@44 340 }
paddy@44 341 }
paddy@44 342 }
paddy@44 343 return nil
paddy@44 344 }
paddy@44 345
paddy@161 346 func (m *memstore) deleteProfile(id uuid.ID) error {
paddy@161 347 m.profileLock.Lock()
paddy@161 348 defer m.profileLock.Unlock()
paddy@161 349 if _, ok := m.profiles[id.String()]; !ok {
paddy@161 350 return ErrProfileNotFound
paddy@161 351 }
paddy@161 352 delete(m.profiles, id.String())
paddy@161 353 return nil
paddy@161 354 }
paddy@161 355
paddy@57 356 func (m *memstore) addLogin(login Login) error {
paddy@44 357 m.loginLock.Lock()
paddy@44 358 defer m.loginLock.Unlock()
paddy@69 359 _, ok := m.logins[login.Value]
paddy@44 360 if ok {
paddy@44 361 return ErrLoginAlreadyExists
paddy@44 362 }
paddy@69 363 m.logins[login.Value] = login
paddy@69 364 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
paddy@44 365 return nil
paddy@44 366 }
paddy@44 367
paddy@172 368 func (m *memstore) getLogin(value string) (Login, error) {
paddy@172 369 m.loginLock.RLock()
paddy@172 370 defer m.loginLock.RUnlock()
paddy@172 371 l, ok := m.logins[value]
paddy@172 372 if !ok {
paddy@172 373 return Login{}, ErrLoginNotFound
paddy@172 374 }
paddy@172 375 return l, nil
paddy@172 376 }
paddy@172 377
paddy@69 378 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
paddy@44 379 m.loginLock.Lock()
paddy@44 380 defer m.loginLock.Unlock()
paddy@69 381 l, ok := m.logins[value]
paddy@44 382 if !ok {
paddy@44 383 return ErrLoginNotFound
paddy@44 384 }
paddy@44 385 if !l.ProfileID.Equal(profile) {
paddy@44 386 return ErrLoginNotFound
paddy@44 387 }
paddy@69 388 delete(m.logins, value)
paddy@44 389 pos := -1
paddy@44 390 for p, id := range m.profileLoginLookup[profile.String()] {
paddy@69 391 if id == value {
paddy@44 392 pos = p
paddy@44 393 break
paddy@44 394 }
paddy@44 395 }
paddy@44 396 if pos >= 0 {
paddy@44 397 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
paddy@44 398 }
paddy@44 399 return nil
paddy@44 400 }
paddy@44 401
paddy@160 402 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
paddy@160 403 m.loginLock.Lock()
paddy@160 404 defer m.loginLock.Unlock()
paddy@160 405 logins, ok := m.profileLoginLookup[profile.String()]
paddy@160 406 if !ok {
paddy@160 407 return ErrProfileNotFound
paddy@160 408 }
paddy@160 409 delete(m.profileLoginLookup, profile.String())
paddy@160 410 for _, login := range logins {
paddy@160 411 delete(m.logins, login)
paddy@160 412 }
paddy@160 413 return nil
paddy@160 414 }
paddy@160 415
paddy@69 416 func (m *memstore) recordLoginUse(value string, when time.Time) error {
paddy@44 417 m.loginLock.Lock()
paddy@44 418 defer m.loginLock.Unlock()
paddy@69 419 l, ok := m.logins[value]
paddy@44 420 if !ok {
paddy@44 421 return ErrLoginNotFound
paddy@44 422 }
paddy@44 423 l.LastUsed = when
paddy@69 424 m.logins[value] = l
paddy@44 425 return nil
paddy@44 426 }
paddy@44 427
paddy@172 428 func (m *memstore) verifyLogin(value, verification string) error {
paddy@172 429 m.loginLock.Lock()
paddy@172 430 defer m.loginLock.Unlock()
paddy@172 431 l, ok := m.logins[value]
paddy@172 432 if !ok {
paddy@172 433 return ErrLoginNotFound
paddy@172 434 }
paddy@172 435 if l.Verification != verification {
paddy@172 436 return ErrLoginVerificationInvalid
paddy@172 437 }
paddy@172 438 l.Verified = true
paddy@172 439 m.logins[value] = l
paddy@172 440 return nil
paddy@172 441 }
paddy@172 442
paddy@57 443 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
paddy@44 444 m.loginLock.RLock()
paddy@44 445 defer m.loginLock.RUnlock()
paddy@44 446 ids, ok := m.profileLoginLookup[profile.String()]
paddy@44 447 if !ok {
paddy@44 448 return []Login{}, nil
paddy@44 449 }
paddy@44 450 if len(ids) > num+offset {
paddy@44 451 ids = ids[offset : num+offset]
paddy@44 452 } else if len(ids) > offset {
paddy@44 453 ids = ids[offset:]
paddy@44 454 } else {
paddy@44 455 return []Login{}, nil
paddy@44 456 }
paddy@44 457 logins := []Login{}
paddy@44 458 for _, id := range ids {
paddy@44 459 login, ok := m.logins[id]
paddy@44 460 if !ok {
paddy@44 461 continue
paddy@44 462 }
paddy@44 463 logins = append(logins, login)
paddy@44 464 }
paddy@44 465 return logins, nil
paddy@44 466 }
paddy@99 467
paddy@160 468 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
paddy@160 469 err := context.RemoveLoginsByProfile(profile)
paddy@160 470 if err != nil {
paddy@160 471 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
paddy@160 472 }
paddy@162 473 err = context.TerminateSessionsByProfile(profile)
paddy@162 474 if err != nil {
paddy@162 475 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
paddy@162 476 }
paddy@162 477 err = context.RevokeTokensByProfileID(profile)
paddy@162 478 if err != nil {
paddy@162 479 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
paddy@162 480 }
paddy@163 481 err = context.DeleteAuthorizationCodesByProfileID(profile)
paddy@163 482 if err != nil {
paddy@163 483 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
paddy@163 484 }
paddy@164 485 clients, err := context.ListClientsByOwner(profile, -1, 0)
paddy@164 486 if err != nil {
paddy@164 487 log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
paddy@164 488 }
paddy@164 489 err = context.DeleteClientsByOwner(profile)
paddy@164 490 if err != nil {
paddy@164 491 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
paddy@164 492 }
paddy@164 493 for _, client := range clients {
paddy@164 494 cleanUpAfterClientDeletion(client.ID, context)
paddy@164 495 }
paddy@160 496 }
paddy@160 497
paddy@105 498 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
paddy@105 499 func RegisterProfileHandlers(r *mux.Router, context Context) {
paddy@165 500 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS")
paddy@166 501 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS")
paddy@165 502 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS")
paddy@165 503 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS")
paddy@128 504 // BUG(paddy): We need to implement a handler that will add a login to a profile.
paddy@128 505 // 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 506 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
paddy@172 507 r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS")
paddy@172 508 r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS")
paddy@105 509 }
paddy@105 510
paddy@166 511 // GetProfileHandler is an HTTP handler for retrieving a profile.
paddy@166 512 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 513 errors := []RequestError{}
paddy@166 514 authz := r.Header.Get("Authorization")
paddy@166 515 if !strings.HasPrefix(authz, "Bearer ") {
paddy@172 516 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 517 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@166 518 return
paddy@166 519 }
paddy@166 520 authz = strings.TrimPrefix(authz, "Bearer ")
paddy@166 521 vars := mux.Vars(r)
paddy@166 522 if vars["id"] == "" {
paddy@172 523 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
paddy@172 524 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@166 525 return
paddy@166 526 }
paddy@166 527 id, err := uuid.Parse(vars["id"])
paddy@166 528 if err != nil {
paddy@172 529 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
paddy@172 530 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@166 531 return
paddy@166 532 }
paddy@166 533 token, err := context.GetToken(authz, false)
paddy@166 534 if err != nil || token.Revoked {
paddy@166 535 if err == ErrTokenNotFound || token.Revoked {
paddy@172 536 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 537 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@166 538 return
paddy@166 539 } else {
paddy@166 540 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@166 541 return
paddy@166 542 }
paddy@166 543 }
paddy@166 544 if !id.Equal(token.ProfileID) {
paddy@172 545 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 546 encode(w, r, http.StatusForbidden, Response{Errors: errors})
paddy@166 547 return
paddy@166 548 }
paddy@166 549 profile, err := context.GetProfileByID(id)
paddy@166 550 if err != nil {
paddy@166 551 if err == ErrProfileNotFound {
paddy@172 552 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
paddy@172 553 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@166 554 return
paddy@166 555 }
paddy@166 556 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@166 557 return
paddy@166 558 }
paddy@172 559 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
paddy@166 560 return
paddy@166 561 }
paddy@166 562
paddy@99 563 // CreateProfileHandler is an HTTP handler for registering new profiles.
paddy@99 564 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@99 565 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@99 566 if !ok {
paddy@149 567 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
paddy@105 568 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 569 return
paddy@99 570 }
paddy@99 571 var req newProfileRequest
paddy@172 572 errors := []RequestError{}
paddy@99 573 decoder := json.NewDecoder(r.Body)
paddy@99 574 err := decoder.Decode(&req)
paddy@99 575 if err != nil {
paddy@105 576 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@99 577 return
paddy@99 578 }
paddy@99 579 errors = append(errors, validateNewProfileRequest(&req)...)
paddy@99 580 if len(errors) > 0 {
paddy@172 581 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@99 582 return
paddy@99 583 }
paddy@99 584 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
paddy@99 585 if err != nil {
paddy@149 586 log.Printf("Error creating encoded passphrase: %#+v\n", err)
paddy@105 587 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 588 return
paddy@99 589 }
paddy@99 590 profile := Profile{
paddy@99 591 ID: uuid.NewID(),
paddy@99 592 Name: req.Name,
paddy@99 593 Passphrase: string(passphrase),
paddy@99 594 Iterations: context.config.iterations,
paddy@99 595 Salt: string(salt),
paddy@99 596 PassphraseScheme: CurPassphraseScheme,
paddy@99 597 Created: time.Now(),
paddy@99 598 LastSeen: time.Now(),
paddy@99 599 }
paddy@99 600 err = context.SaveProfile(profile)
paddy@99 601 if err != nil {
paddy@105 602 if err == ErrProfileAlreadyExists {
paddy@172 603 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}})
paddy@105 604 return
paddy@105 605 }
paddy@149 606 log.Printf("Error saving profile: %#+v\n", err)
paddy@105 607 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 608 return
paddy@99 609 }
paddy@99 610 logins := []Login{}
paddy@99 611 login := Login{
paddy@172 612 Type: "email",
paddy@172 613 Value: req.Email,
paddy@172 614 Created: profile.Created,
paddy@172 615 LastUsed: profile.Created,
paddy@172 616 ProfileID: profile.ID,
paddy@172 617 Verification: uuid.NewID().String(),
paddy@99 618 }
paddy@99 619 err = context.AddLogin(login)
paddy@99 620 if err != nil {
paddy@105 621 if err == ErrLoginAlreadyExists {
paddy@172 622 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}})
paddy@105 623 return
paddy@105 624 }
paddy@149 625 log.Printf("Error adding login: %#+v\n", err)
paddy@105 626 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 627 return
paddy@99 628 }
paddy@99 629 logins = append(logins, login)
paddy@172 630 resp := Response{
paddy@105 631 Logins: logins,
paddy@105 632 Profiles: []Profile{profile},
paddy@105 633 }
paddy@105 634 encode(w, r, http.StatusCreated, resp)
paddy@177 635 go context.SendLoginVerification(login) // this should key off that model event, instead
paddy@177 636 //BUG(paddy): Need to trigger a ModelEvent saying the profile was created
paddy@177 637 // go context.SendModelEvent(profile, events.ActionCreated)
paddy@99 638 }
paddy@145 639
paddy@145 640 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 641 errors := []RequestError{}
paddy@145 642 vars := mux.Vars(r)
paddy@145 643 if vars["id"] == "" {
paddy@172 644 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
paddy@172 645 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 646 return
paddy@145 647 }
paddy@145 648 id, err := uuid.Parse(vars["id"])
paddy@145 649 if err != nil {
paddy@172 650 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 651 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 652 return
paddy@145 653 }
paddy@145 654 username, password, ok := r.BasicAuth()
paddy@145 655 if !ok {
paddy@172 656 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 657 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@145 658 return
paddy@145 659 }
paddy@145 660 profile, err := authenticate(username, password, context)
paddy@145 661 if err != nil {
paddy@145 662 if isAuthError(err) {
paddy@172 663 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 664 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@145 665 } else {
paddy@172 666 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
paddy@172 667 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
paddy@145 668 }
paddy@145 669 return
paddy@145 670 }
paddy@145 671 if !profile.ID.Equal(id) {
paddy@172 672 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 673 encode(w, r, http.StatusForbidden, Response{Errors: errors})
paddy@145 674 return
paddy@145 675 }
paddy@145 676 var req ProfileChange
paddy@145 677 decoder := json.NewDecoder(r.Body)
paddy@145 678 err = decoder.Decode(&req)
paddy@145 679 if err != nil {
paddy@149 680 log.Printf("Error decoding request: %#+v\n", err)
paddy@145 681 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@145 682 return
paddy@145 683 }
paddy@145 684 req.Iterations = nil
paddy@145 685 req.Salt = nil
paddy@145 686 req.PassphraseScheme = nil
paddy@145 687 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
paddy@145 688 req.LockedUntil = nil
paddy@145 689 req.LastSeen = nil
paddy@145 690 if req.Passphrase != nil {
paddy@145 691 if len(*req.Passphrase) < MinPassphraseLength {
paddy@172 692 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"})
paddy@172 693 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 694 return
paddy@145 695 }
paddy@145 696 if len(*req.Passphrase) > MaxPassphraseLength {
paddy@172 697 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"})
paddy@172 698 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 699 return
paddy@145 700 }
paddy@145 701 iterations := context.config.iterations
paddy@145 702 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@145 703 if !ok {
paddy@145 704 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 705 return
paddy@145 706 }
paddy@145 707 curScheme := CurPassphraseScheme
paddy@145 708 req.PassphraseScheme = &curScheme
paddy@145 709 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
paddy@145 710 if err != nil {
paddy@145 711 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 712 return
paddy@145 713 }
paddy@145 714 req.Passphrase = &passphrase
paddy@145 715 req.Salt = &salt
paddy@145 716 req.Iterations = &iterations
paddy@145 717 }
paddy@145 718 if req.PassphraseReset != nil {
paddy@145 719 now := time.Now()
paddy@145 720 req.PassphraseResetCreated = &now
paddy@145 721 }
paddy@145 722 err = req.Validate()
paddy@145 723 if err != nil {
paddy@145 724 var status int
paddy@172 725 var resp Response
paddy@145 726 switch err {
paddy@145 727 case ErrEmptyChange:
paddy@145 728 resp.Profiles = []Profile{profile}
paddy@145 729 status = http.StatusOK
paddy@145 730 default:
paddy@172 731 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
paddy@145 732 resp.Errors = errors
paddy@145 733 status = http.StatusInternalServerError
paddy@145 734 }
paddy@145 735 encode(w, r, status, resp)
paddy@145 736 return
paddy@145 737 }
paddy@145 738 err = context.UpdateProfile(id, req)
paddy@145 739 if err != nil {
paddy@145 740 if err == ErrProfileNotFound {
paddy@172 741 errors = append(errors, RequestError{Slug: RequestErrNotFound})
paddy@172 742 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@145 743 return
paddy@145 744 }
paddy@145 745 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 746 return
paddy@145 747 }
paddy@145 748 profile.ApplyChange(req)
paddy@172 749 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
paddy@145 750 return
paddy@145 751 }
paddy@160 752
paddy@160 753 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 754 errors := []RequestError{}
paddy@160 755 vars := mux.Vars(r)
paddy@160 756 if vars["id"] == "" {
paddy@172 757 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
paddy@172 758 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@160 759 return
paddy@160 760 }
paddy@160 761 id, err := uuid.Parse(vars["id"])
paddy@160 762 if err != nil {
paddy@172 763 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 764 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@160 765 return
paddy@160 766 }
paddy@160 767 username, password, ok := r.BasicAuth()
paddy@160 768 if !ok {
paddy@172 769 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 770 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@160 771 return
paddy@160 772 }
paddy@160 773 profile, err := authenticate(username, password, context)
paddy@160 774 if err != nil {
paddy@160 775 if isAuthError(err) {
paddy@172 776 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 777 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@160 778 } else {
paddy@172 779 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
paddy@172 780 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
paddy@160 781 }
paddy@160 782 return
paddy@160 783 }
paddy@160 784 if !profile.ID.Equal(id) {
paddy@172 785 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 786 encode(w, r, http.StatusForbidden, Response{Errors: errors})
paddy@160 787 return
paddy@160 788 }
paddy@161 789 err = context.DeleteProfile(id)
paddy@160 790 if err != nil {
paddy@160 791 if err == ErrProfileNotFound {
paddy@172 792 errors = append(errors, RequestError{Slug: RequestErrNotFound})
paddy@172 793 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@160 794 return
paddy@160 795 }
paddy@160 796 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@160 797 return
paddy@160 798 }
paddy@172 799 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
paddy@160 800 go cleanUpAfterProfileDeletion(profile.ID, context)
paddy@160 801 }
paddy@172 802
paddy@172 803 func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 804 var errors []RequestError
paddy@172 805 vars := mux.Vars(r)
paddy@172 806 if vars["login"] == "" {
paddy@172 807 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
paddy@172 808 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 809 return
paddy@172 810 }
paddy@172 811 login, err := context.GetLogin(vars["login"])
paddy@172 812 if err != nil {
paddy@172 813 if err == ErrLoginNotFound {
paddy@172 814 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
paddy@172 815 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@172 816 return
paddy@172 817 }
paddy@172 818 log.Printf("Error retrieving login: %#+v\n", err)
paddy@172 819 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@172 820 return
paddy@172 821 }
paddy@172 822 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
paddy@172 823 }
paddy@172 824
paddy@172 825 func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 826 var errors []RequestError
paddy@172 827 vars := mux.Vars(r)
paddy@172 828 if vars["login"] == "" {
paddy@172 829 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
paddy@172 830 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 831 return
paddy@172 832 }
paddy@172 833 var req LoginChange
paddy@172 834 decoder := json.NewDecoder(r.Body)
paddy@172 835 err := decoder.Decode(&req)
paddy@172 836 if err != nil {
paddy@172 837 log.Printf("Error decoding request: %#+v\n", err)
paddy@172 838 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@172 839 return
paddy@172 840 }
paddy@172 841 login, err := context.GetLogin(vars["login"])
paddy@172 842 if err != nil {
paddy@172 843 if err == ErrLoginNotFound {
paddy@172 844 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
paddy@172 845 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@172 846 return
paddy@172 847 }
paddy@172 848 log.Printf("Error retrieving login: %#+v\n", err)
paddy@172 849 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@172 850 return
paddy@172 851 }
paddy@172 852 if req.Verification != nil {
paddy@172 853 err = context.VerifyLogin(vars["login"], *req.Verification)
paddy@172 854 if err != nil {
paddy@172 855 if err == ErrLoginNotFound {
paddy@172 856 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
paddy@172 857 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@172 858 return
paddy@172 859 } else if err == ErrLoginVerificationInvalid {
paddy@172 860 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"})
paddy@172 861 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 862 return
paddy@172 863 }
paddy@172 864 log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err)
paddy@172 865 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@172 866 return
paddy@172 867 }
paddy@172 868 login.Verified = true
paddy@172 869 } else if req.ResendVerification != nil {
paddy@172 870 if !*req.ResendVerification {
paddy@172 871 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"})
paddy@172 872 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 873 return
paddy@172 874 }
paddy@172 875 context.SendLoginVerification(login)
paddy@172 876 } else {
paddy@172 877 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
paddy@172 878 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 879 return
paddy@172 880 }
paddy@172 881 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
paddy@172 882 }