auth

Paddy 2015-12-14 Parent:b7e685839a1b

182:cd5f07f9811b Go to Latest

auth/profile.go

Update nsq import path. go-nsq has moved to nsqio/go-nsq, so we need to update the import path appropriately.

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@181 12 "code.secondbit.org/auth.hg/events"
paddy@178 13 "code.secondbit.org/events.hg"
paddy@181 14 "code.secondbit.org/scopes.hg/types"
paddy@107 15 "code.secondbit.org/uuid.hg"
paddy@178 16
paddy@105 17 "github.com/gorilla/mux"
paddy@27 18 )
paddy@27 19
paddy@48 20 const (
paddy@57 21 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive.
paddy@48 22 MinPassphraseLength = 6
paddy@57 23 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive.
paddy@48 24 MaxPassphraseLength = 64
paddy@69 25 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme
paddy@69 26 CurPassphraseScheme = 1
paddy@99 27 // MaxNameLength is the maximum length, in bytes, of a name, exclusive.
paddy@99 28 MaxNameLength = 64
paddy@99 29 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
paddy@99 30 MaxEmailLength = 64
paddy@48 31 )
paddy@48 32
paddy@38 33 var (
paddy@57 34 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
paddy@57 35 ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
paddy@57 36 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
paddy@57 37 // the same ID already exists in the profileStore.
paddy@57 38 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
paddy@57 39 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
paddy@57 40 ErrProfileNotFound = errors.New("profile not found in profileStore")
paddy@57 41 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
paddy@57 42 // Type and Value already exists in the profileStore.
paddy@57 43 ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
paddy@57 44 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
paddy@57 45 ErrLoginNotFound = errors.New("login not found in profileStore")
paddy@172 46 // ErrLoginVerificationInvalid is returned when a Login is verified with the wrong verification code.
paddy@172 47 ErrLoginVerificationInvalid = errors.New("login verification code incorrect")
paddy@48 48
paddy@57 49 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
paddy@57 50 // Passphrase, and requires one.
paddy@57 51 ErrMissingPassphrase = errors.New("missing passphrase")
paddy@57 52 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
paddy@57 53 // a PassphraseReset, and requires one.
paddy@57 54 ErrMissingPassphraseReset = errors.New("missing passphrase reset")
paddy@57 55 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
paddy@57 56 // contain a PassphraseResetCreated, and requires one.
paddy@48 57 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
paddy@57 58 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
paddy@57 59 // but the Passphrase is shorter than MinPassphraseLength.
paddy@57 60 ErrPassphraseTooShort = errors.New("passphrase too short")
paddy@57 61 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
paddy@57 62 // but the Passphrase is longer than MaxPassphraseLength.
paddy@57 63 ErrPassphraseTooLong = errors.New("passphrase too long")
paddy@99 64
paddy@99 65 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected
paddy@99 66 // of being compromised.
paddy@99 67 ErrProfileCompromised = errors.New("profile compromised")
paddy@99 68 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain
paddy@99 69 // duration, to prevent brute force attacks.
paddy@99 70 ErrProfileLocked = errors.New("profile locked")
paddy@173 71
paddy@181 72 ScopeLoginAdmin = scopeTypes.Scope{ID: "login_admin", Name: "Administer Logins", Description: "Read and write logins, bypassing ACL."}
paddy@38 73 )
paddy@38 74
paddy@57 75 // Profile represents a single user of the service,
paddy@158 76 // including their authentication information.
paddy@27 77 type Profile struct {
paddy@105 78 ID uuid.ID `json:"id,omitempty"`
paddy@105 79 Name string `json:"name,omitempty"`
paddy@105 80 Passphrase string `json:"-"`
paddy@105 81 Iterations int `json:"-"`
paddy@105 82 Salt string `json:"-"`
paddy@105 83 PassphraseScheme int `json:"-"`
paddy@105 84 Compromised bool `json:"-"`
paddy@105 85 LockedUntil time.Time `json:"-"`
paddy@105 86 PassphraseReset string `json:"-"`
paddy@105 87 PassphraseResetCreated time.Time `json:"-"`
paddy@105 88 Created time.Time `json:"created,omitempty"`
paddy@105 89 LastSeen time.Time `json:"last_seen,omitempty"`
paddy@38 90 }
paddy@38 91
paddy@178 92 func (p Profile) GetModelName() string {
paddy@178 93 return "profiles"
paddy@178 94 }
paddy@178 95
paddy@178 96 func (p Profile) GetID() string {
paddy@178 97 return p.ID.String()
paddy@178 98 }
paddy@178 99
paddy@178 100 func (p Profile) GetSystem() string {
paddy@178 101 return "code.secondbit.org/auth"
paddy@178 102 }
paddy@178 103
paddy@57 104 // ApplyChange applies the properties of the passed ProfileChange
paddy@57 105 // to the Profile it is called on.
paddy@38 106 func (p *Profile) ApplyChange(change ProfileChange) {
paddy@38 107 if change.Name != nil {
paddy@38 108 p.Name = *change.Name
paddy@38 109 }
paddy@38 110 if change.Passphrase != nil {
paddy@38 111 p.Passphrase = *change.Passphrase
paddy@38 112 }
paddy@38 113 if change.Iterations != nil {
paddy@38 114 p.Iterations = *change.Iterations
paddy@38 115 }
paddy@38 116 if change.Salt != nil {
paddy@38 117 p.Salt = *change.Salt
paddy@38 118 }
paddy@38 119 if change.PassphraseScheme != nil {
paddy@38 120 p.PassphraseScheme = *change.PassphraseScheme
paddy@38 121 }
paddy@38 122 if change.Compromised != nil {
paddy@38 123 p.Compromised = *change.Compromised
paddy@38 124 }
paddy@38 125 if change.LockedUntil != nil {
paddy@38 126 p.LockedUntil = *change.LockedUntil
paddy@38 127 }
paddy@38 128 if change.PassphraseReset != nil {
paddy@38 129 p.PassphraseReset = *change.PassphraseReset
paddy@38 130 }
paddy@38 131 if change.PassphraseResetCreated != nil {
paddy@38 132 p.PassphraseResetCreated = *change.PassphraseResetCreated
paddy@38 133 }
paddy@38 134 if change.LastSeen != nil {
paddy@38 135 p.LastSeen = *change.LastSeen
paddy@38 136 }
paddy@38 137 }
paddy@38 138
paddy@57 139 // ApplyBulkChange applies the properties of the passed BulkProfileChange
paddy@57 140 // to the Profile it is called on.
paddy@44 141 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
paddy@44 142 if change.Compromised != nil {
paddy@44 143 p.Compromised = *change.Compromised
paddy@44 144 }
paddy@44 145 }
paddy@44 146
paddy@57 147 // ProfileChange represents a single atomic change to a Profile's mutable data.
paddy@38 148 type ProfileChange struct {
paddy@38 149 Name *string
paddy@38 150 Passphrase *string
paddy@69 151 Iterations *int
paddy@38 152 Salt *string
paddy@38 153 PassphraseScheme *int
paddy@38 154 Compromised *bool
paddy@38 155 LockedUntil *time.Time
paddy@38 156 PassphraseReset *string
paddy@38 157 PassphraseResetCreated *time.Time
paddy@38 158 LastSeen *time.Time
paddy@38 159 }
paddy@38 160
paddy@149 161 func (c ProfileChange) Empty() bool {
paddy@161 162 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 163 }
paddy@149 164
paddy@57 165 // Validate checks the ProfileChange it is called on
paddy@57 166 // and asserts its internal validity, or lack thereof.
paddy@57 167 // A descriptive error will be returned in the case of
paddy@57 168 // an invalid change.
paddy@38 169 func (c ProfileChange) Validate() error {
paddy@149 170 if c.Empty() {
paddy@48 171 return ErrEmptyChange
paddy@48 172 }
paddy@48 173 if c.PassphraseScheme != nil && c.Passphrase == nil {
paddy@48 174 return ErrMissingPassphrase
paddy@48 175 }
paddy@48 176 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
paddy@48 177 return ErrMissingPassphraseResetCreated
paddy@48 178 }
paddy@48 179 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
paddy@48 180 return ErrMissingPassphraseReset
paddy@48 181 }
paddy@48 182 if c.Salt != nil && c.Passphrase == nil {
paddy@48 183 return ErrMissingPassphrase
paddy@48 184 }
paddy@48 185 if c.Iterations != nil && c.Passphrase == nil {
paddy@48 186 return ErrMissingPassphrase
paddy@48 187 }
paddy@38 188 return nil
paddy@27 189 }
paddy@27 190
paddy@57 191 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
paddy@57 192 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
paddy@57 193 // ProfileChange values across many Profiles all at once.
paddy@44 194 type BulkProfileChange struct {
paddy@44 195 Compromised *bool
paddy@44 196 }
paddy@44 197
paddy@149 198 func (b BulkProfileChange) Empty() bool {
paddy@149 199 return b.Compromised == nil
paddy@149 200 }
paddy@149 201
paddy@57 202 // Validate checks the BulkProfileChange it is called on
paddy@57 203 // and asserts its internal validity, or lack thereof.
paddy@57 204 // A descriptive error will be returned in the case of an
paddy@57 205 // invalid change.
paddy@44 206 func (b BulkProfileChange) Validate() error {
paddy@149 207 if b.Empty() {
paddy@48 208 return ErrEmptyChange
paddy@48 209 }
paddy@44 210 return nil
paddy@44 211 }
paddy@44 212
paddy@57 213 // Login represents a single human-friendly identifier for
paddy@57 214 // a given Profile that can be used to log into that Profile.
paddy@57 215 // Each Profile may only have one Login for each Type.
paddy@27 216 type Login struct {
paddy@172 217 Type string `json:"type,omitempty"`
paddy@172 218 Value string `json:"value,omitempty"`
paddy@172 219 ProfileID uuid.ID `json:"profile_id,omitempty"`
paddy@172 220 Created time.Time `json:"created,omitempty"`
paddy@172 221 LastUsed time.Time `json:"last_used,omitempty"`
paddy@178 222 Verification string `json:"verification,omitempty"`
paddy@172 223 Verified bool `json:"verified"`
paddy@172 224 }
paddy@172 225
paddy@178 226 func (l Login) GetModelName() string {
paddy@178 227 return "logins"
paddy@178 228 }
paddy@178 229
paddy@178 230 func (l Login) GetID() string {
paddy@178 231 return l.Value
paddy@178 232 }
paddy@178 233
paddy@178 234 func (l Login) GetSystem() string {
paddy@178 235 return "code.secondbit.org/auth"
paddy@178 236 }
paddy@178 237
paddy@172 238 type LoginChange struct {
paddy@172 239 Verification *string `json:"verification,omitempty"`
paddy@172 240 ResendVerification *bool `json:"resend_verification,omitempty"`
paddy@27 241 }
paddy@27 242
paddy@99 243 type newProfileRequest struct {
paddy@99 244 Email string `json:"email"`
paddy@99 245 Passphrase string `json:"passphrase"`
paddy@99 246 Name string `json:"name"`
paddy@99 247 }
paddy@99 248
paddy@172 249 func validateNewProfileRequest(req *newProfileRequest) []RequestError {
paddy@172 250 errors := []RequestError{}
paddy@99 251 req.Name = strings.TrimSpace(req.Name)
paddy@99 252 req.Email = strings.TrimSpace(req.Email)
paddy@99 253 if len(req.Passphrase) < MinPassphraseLength {
paddy@172 254 errors = append(errors, RequestError{
paddy@172 255 Slug: RequestErrInsufficient,
paddy@99 256 Field: "/passphrase",
paddy@99 257 })
paddy@99 258 }
paddy@99 259 if len(req.Passphrase) > MaxPassphraseLength {
paddy@172 260 errors = append(errors, RequestError{
paddy@172 261 Slug: RequestErrOverflow,
paddy@99 262 Field: "/passphrase",
paddy@99 263 })
paddy@99 264 }
paddy@99 265 if len(req.Name) > MaxNameLength {
paddy@172 266 errors = append(errors, RequestError{
paddy@172 267 Slug: RequestErrOverflow,
paddy@99 268 Field: "/name",
paddy@99 269 })
paddy@99 270 }
paddy@99 271 if req.Email == "" {
paddy@172 272 errors = append(errors, RequestError{
paddy@172 273 Slug: RequestErrMissing,
paddy@99 274 Field: "/email",
paddy@99 275 })
paddy@99 276 }
paddy@99 277 if len(req.Email) > MaxEmailLength {
paddy@172 278 errors = append(errors, RequestError{
paddy@172 279 Slug: RequestErrOverflow,
paddy@99 280 Field: "/email",
paddy@99 281 })
paddy@99 282 }
paddy@99 283 re := regexp.MustCompile(".+@.+\\..+")
paddy@99 284 if !re.Match([]byte(req.Email)) {
paddy@172 285 errors = append(errors, RequestError{
paddy@172 286 Slug: RequestErrInvalidFormat,
paddy@99 287 Field: "/email",
paddy@99 288 })
paddy@99 289 }
paddy@99 290 return errors
paddy@99 291 }
paddy@99 292
paddy@57 293 type profileStore interface {
paddy@57 294 getProfileByID(id uuid.ID) (Profile, error)
paddy@69 295 getProfileByLogin(value string) (Profile, error)
paddy@57 296 saveProfile(profile Profile) error
paddy@57 297 updateProfile(id uuid.ID, change ProfileChange) error
paddy@57 298 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
paddy@161 299 deleteProfile(id uuid.ID) error
paddy@44 300
paddy@57 301 addLogin(login Login) error
paddy@172 302 getLogin(value string) (Login, error)
paddy@69 303 removeLogin(value string, profile uuid.ID) error
paddy@160 304 removeLoginsByProfile(profile uuid.ID) error
paddy@69 305 recordLoginUse(value string, when time.Time) error
paddy@172 306 verifyLogin(value, verification string) error
paddy@57 307 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
paddy@38 308 }
paddy@27 309
paddy@57 310 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
paddy@38 311 m.profileLock.RLock()
paddy@38 312 defer m.profileLock.RUnlock()
paddy@38 313 p, ok := m.profiles[id.String()]
paddy@38 314 if !ok {
paddy@38 315 return Profile{}, ErrProfileNotFound
paddy@38 316 }
paddy@38 317 return p, nil
paddy@27 318 }
paddy@38 319
paddy@69 320 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
paddy@44 321 m.loginLock.RLock()
paddy@44 322 defer m.loginLock.RUnlock()
paddy@69 323 login, ok := m.logins[value]
paddy@44 324 if !ok {
paddy@44 325 return Profile{}, ErrLoginNotFound
paddy@44 326 }
paddy@44 327 m.profileLock.RLock()
paddy@44 328 defer m.profileLock.RUnlock()
paddy@44 329 profile, ok := m.profiles[login.ProfileID.String()]
paddy@44 330 if !ok {
paddy@44 331 return Profile{}, ErrProfileNotFound
paddy@44 332 }
paddy@44 333 return profile, nil
paddy@38 334 }
paddy@38 335
paddy@57 336 func (m *memstore) saveProfile(profile Profile) error {
paddy@38 337 m.profileLock.Lock()
paddy@38 338 defer m.profileLock.Unlock()
paddy@38 339 _, ok := m.profiles[profile.ID.String()]
paddy@38 340 if ok {
paddy@38 341 return ErrProfileAlreadyExists
paddy@38 342 }
paddy@38 343 m.profiles[profile.ID.String()] = profile
paddy@38 344 return nil
paddy@38 345 }
paddy@38 346
paddy@57 347 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
paddy@38 348 m.profileLock.Lock()
paddy@38 349 defer m.profileLock.Unlock()
paddy@38 350 p, ok := m.profiles[id.String()]
paddy@38 351 if !ok {
paddy@38 352 return ErrProfileNotFound
paddy@38 353 }
paddy@38 354 p.ApplyChange(change)
paddy@38 355 m.profiles[id.String()] = p
paddy@38 356 return nil
paddy@38 357 }
paddy@38 358
paddy@57 359 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
paddy@44 360 m.profileLock.Lock()
paddy@44 361 defer m.profileLock.Unlock()
paddy@44 362 for id, profile := range m.profiles {
paddy@44 363 for _, i := range ids {
paddy@44 364 if id == i.String() {
paddy@44 365 profile.ApplyBulkChange(change)
paddy@44 366 m.profiles[id] = profile
paddy@44 367 break
paddy@44 368 }
paddy@44 369 }
paddy@44 370 }
paddy@44 371 return nil
paddy@44 372 }
paddy@44 373
paddy@161 374 func (m *memstore) deleteProfile(id uuid.ID) error {
paddy@161 375 m.profileLock.Lock()
paddy@161 376 defer m.profileLock.Unlock()
paddy@161 377 if _, ok := m.profiles[id.String()]; !ok {
paddy@161 378 return ErrProfileNotFound
paddy@161 379 }
paddy@161 380 delete(m.profiles, id.String())
paddy@161 381 return nil
paddy@161 382 }
paddy@161 383
paddy@57 384 func (m *memstore) addLogin(login Login) error {
paddy@44 385 m.loginLock.Lock()
paddy@44 386 defer m.loginLock.Unlock()
paddy@69 387 _, ok := m.logins[login.Value]
paddy@44 388 if ok {
paddy@44 389 return ErrLoginAlreadyExists
paddy@44 390 }
paddy@69 391 m.logins[login.Value] = login
paddy@69 392 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
paddy@44 393 return nil
paddy@44 394 }
paddy@44 395
paddy@172 396 func (m *memstore) getLogin(value string) (Login, error) {
paddy@172 397 m.loginLock.RLock()
paddy@172 398 defer m.loginLock.RUnlock()
paddy@172 399 l, ok := m.logins[value]
paddy@172 400 if !ok {
paddy@172 401 return Login{}, ErrLoginNotFound
paddy@172 402 }
paddy@172 403 return l, nil
paddy@172 404 }
paddy@172 405
paddy@69 406 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
paddy@44 407 m.loginLock.Lock()
paddy@44 408 defer m.loginLock.Unlock()
paddy@69 409 l, ok := m.logins[value]
paddy@44 410 if !ok {
paddy@44 411 return ErrLoginNotFound
paddy@44 412 }
paddy@44 413 if !l.ProfileID.Equal(profile) {
paddy@44 414 return ErrLoginNotFound
paddy@44 415 }
paddy@69 416 delete(m.logins, value)
paddy@44 417 pos := -1
paddy@44 418 for p, id := range m.profileLoginLookup[profile.String()] {
paddy@69 419 if id == value {
paddy@44 420 pos = p
paddy@44 421 break
paddy@44 422 }
paddy@44 423 }
paddy@44 424 if pos >= 0 {
paddy@44 425 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
paddy@44 426 }
paddy@44 427 return nil
paddy@44 428 }
paddy@44 429
paddy@160 430 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
paddy@160 431 m.loginLock.Lock()
paddy@160 432 defer m.loginLock.Unlock()
paddy@160 433 logins, ok := m.profileLoginLookup[profile.String()]
paddy@160 434 if !ok {
paddy@160 435 return ErrProfileNotFound
paddy@160 436 }
paddy@160 437 delete(m.profileLoginLookup, profile.String())
paddy@160 438 for _, login := range logins {
paddy@160 439 delete(m.logins, login)
paddy@160 440 }
paddy@160 441 return nil
paddy@160 442 }
paddy@160 443
paddy@69 444 func (m *memstore) recordLoginUse(value string, when time.Time) error {
paddy@44 445 m.loginLock.Lock()
paddy@44 446 defer m.loginLock.Unlock()
paddy@69 447 l, ok := m.logins[value]
paddy@44 448 if !ok {
paddy@44 449 return ErrLoginNotFound
paddy@44 450 }
paddy@44 451 l.LastUsed = when
paddy@69 452 m.logins[value] = l
paddy@44 453 return nil
paddy@44 454 }
paddy@44 455
paddy@172 456 func (m *memstore) verifyLogin(value, verification string) error {
paddy@172 457 m.loginLock.Lock()
paddy@172 458 defer m.loginLock.Unlock()
paddy@172 459 l, ok := m.logins[value]
paddy@172 460 if !ok {
paddy@172 461 return ErrLoginNotFound
paddy@172 462 }
paddy@172 463 if l.Verification != verification {
paddy@172 464 return ErrLoginVerificationInvalid
paddy@172 465 }
paddy@172 466 l.Verified = true
paddy@172 467 m.logins[value] = l
paddy@172 468 return nil
paddy@172 469 }
paddy@172 470
paddy@57 471 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
paddy@44 472 m.loginLock.RLock()
paddy@44 473 defer m.loginLock.RUnlock()
paddy@44 474 ids, ok := m.profileLoginLookup[profile.String()]
paddy@44 475 if !ok {
paddy@44 476 return []Login{}, nil
paddy@44 477 }
paddy@44 478 if len(ids) > num+offset {
paddy@44 479 ids = ids[offset : num+offset]
paddy@44 480 } else if len(ids) > offset {
paddy@44 481 ids = ids[offset:]
paddy@44 482 } else {
paddy@44 483 return []Login{}, nil
paddy@44 484 }
paddy@44 485 logins := []Login{}
paddy@44 486 for _, id := range ids {
paddy@44 487 login, ok := m.logins[id]
paddy@44 488 if !ok {
paddy@44 489 continue
paddy@44 490 }
paddy@44 491 logins = append(logins, login)
paddy@44 492 }
paddy@44 493 return logins, nil
paddy@44 494 }
paddy@99 495
paddy@160 496 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
paddy@160 497 err := context.RemoveLoginsByProfile(profile)
paddy@160 498 if err != nil {
paddy@160 499 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
paddy@160 500 }
paddy@162 501 err = context.TerminateSessionsByProfile(profile)
paddy@162 502 if err != nil {
paddy@162 503 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
paddy@162 504 }
paddy@162 505 err = context.RevokeTokensByProfileID(profile)
paddy@162 506 if err != nil {
paddy@162 507 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
paddy@162 508 }
paddy@163 509 err = context.DeleteAuthorizationCodesByProfileID(profile)
paddy@163 510 if err != nil {
paddy@163 511 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
paddy@163 512 }
paddy@164 513 clients, err := context.ListClientsByOwner(profile, -1, 0)
paddy@164 514 if err != nil {
paddy@164 515 log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
paddy@164 516 }
paddy@164 517 err = context.DeleteClientsByOwner(profile)
paddy@164 518 if err != nil {
paddy@164 519 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
paddy@164 520 }
paddy@164 521 for _, client := range clients {
paddy@164 522 cleanUpAfterClientDeletion(client.ID, context)
paddy@164 523 }
paddy@160 524 }
paddy@160 525
paddy@105 526 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
paddy@105 527 func RegisterProfileHandlers(r *mux.Router, context Context) {
paddy@165 528 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS")
paddy@166 529 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS")
paddy@165 530 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS")
paddy@165 531 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS")
paddy@128 532 // BUG(paddy): We need to implement a handler that will add a login to a profile.
paddy@128 533 // 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 534 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
paddy@172 535 r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS")
paddy@172 536 r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS")
paddy@105 537 }
paddy@105 538
paddy@166 539 // GetProfileHandler is an HTTP handler for retrieving a profile.
paddy@166 540 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 541 errors := []RequestError{}
paddy@166 542 authz := r.Header.Get("Authorization")
paddy@166 543 if !strings.HasPrefix(authz, "Bearer ") {
paddy@172 544 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 545 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@166 546 return
paddy@166 547 }
paddy@166 548 authz = strings.TrimPrefix(authz, "Bearer ")
paddy@166 549 vars := mux.Vars(r)
paddy@166 550 if vars["id"] == "" {
paddy@172 551 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
paddy@172 552 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@166 553 return
paddy@166 554 }
paddy@166 555 id, err := uuid.Parse(vars["id"])
paddy@166 556 if err != nil {
paddy@172 557 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
paddy@172 558 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@166 559 return
paddy@166 560 }
paddy@166 561 token, err := context.GetToken(authz, false)
paddy@166 562 if err != nil || token.Revoked {
paddy@166 563 if err == ErrTokenNotFound || token.Revoked {
paddy@172 564 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 565 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@166 566 return
paddy@166 567 } else {
paddy@166 568 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@166 569 return
paddy@166 570 }
paddy@166 571 }
paddy@166 572 if !id.Equal(token.ProfileID) {
paddy@172 573 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 574 encode(w, r, http.StatusForbidden, Response{Errors: errors})
paddy@166 575 return
paddy@166 576 }
paddy@166 577 profile, err := context.GetProfileByID(id)
paddy@166 578 if err != nil {
paddy@166 579 if err == ErrProfileNotFound {
paddy@172 580 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
paddy@172 581 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@166 582 return
paddy@166 583 }
paddy@166 584 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@166 585 return
paddy@166 586 }
paddy@172 587 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
paddy@166 588 return
paddy@166 589 }
paddy@166 590
paddy@99 591 // CreateProfileHandler is an HTTP handler for registering new profiles.
paddy@99 592 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@99 593 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@99 594 if !ok {
paddy@149 595 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
paddy@105 596 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 597 return
paddy@99 598 }
paddy@99 599 var req newProfileRequest
paddy@172 600 errors := []RequestError{}
paddy@99 601 decoder := json.NewDecoder(r.Body)
paddy@99 602 err := decoder.Decode(&req)
paddy@99 603 if err != nil {
paddy@105 604 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@99 605 return
paddy@99 606 }
paddy@99 607 errors = append(errors, validateNewProfileRequest(&req)...)
paddy@99 608 if len(errors) > 0 {
paddy@172 609 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@99 610 return
paddy@99 611 }
paddy@99 612 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
paddy@99 613 if err != nil {
paddy@149 614 log.Printf("Error creating encoded passphrase: %#+v\n", err)
paddy@105 615 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 616 return
paddy@99 617 }
paddy@99 618 profile := Profile{
paddy@99 619 ID: uuid.NewID(),
paddy@99 620 Name: req.Name,
paddy@99 621 Passphrase: string(passphrase),
paddy@99 622 Iterations: context.config.iterations,
paddy@99 623 Salt: string(salt),
paddy@99 624 PassphraseScheme: CurPassphraseScheme,
paddy@99 625 Created: time.Now(),
paddy@99 626 LastSeen: time.Now(),
paddy@99 627 }
paddy@99 628 err = context.SaveProfile(profile)
paddy@99 629 if err != nil {
paddy@105 630 if err == ErrProfileAlreadyExists {
paddy@172 631 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}})
paddy@105 632 return
paddy@105 633 }
paddy@149 634 log.Printf("Error saving profile: %#+v\n", err)
paddy@105 635 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 636 return
paddy@99 637 }
paddy@99 638 logins := []Login{}
paddy@99 639 login := Login{
paddy@172 640 Type: "email",
paddy@172 641 Value: req.Email,
paddy@172 642 Created: profile.Created,
paddy@172 643 LastUsed: profile.Created,
paddy@172 644 ProfileID: profile.ID,
paddy@172 645 Verification: uuid.NewID().String(),
paddy@99 646 }
paddy@99 647 err = context.AddLogin(login)
paddy@99 648 if err != nil {
paddy@105 649 if err == ErrLoginAlreadyExists {
paddy@172 650 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}})
paddy@105 651 return
paddy@105 652 }
paddy@149 653 log.Printf("Error adding login: %#+v\n", err)
paddy@105 654 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 655 return
paddy@99 656 }
paddy@178 657 verification := login.Verification
paddy@178 658 login.Verification = "" // clear verification so it's not exposed
paddy@99 659 logins = append(logins, login)
paddy@172 660 resp := Response{
paddy@105 661 Logins: logins,
paddy@105 662 Profiles: []Profile{profile},
paddy@105 663 }
paddy@105 664 encode(w, r, http.StatusCreated, resp)
paddy@178 665 login.Verification = verification // restore verification so it's included in the event
paddy@178 666 go context.SendModelEvent(login, events.ActionCreated)
paddy@178 667 go context.SendModelEvent(profile, events.ActionCreated)
paddy@99 668 }
paddy@145 669
paddy@145 670 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 671 errors := []RequestError{}
paddy@145 672 vars := mux.Vars(r)
paddy@145 673 if vars["id"] == "" {
paddy@172 674 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
paddy@172 675 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 676 return
paddy@145 677 }
paddy@145 678 id, err := uuid.Parse(vars["id"])
paddy@145 679 if err != nil {
paddy@172 680 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 681 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 682 return
paddy@145 683 }
paddy@145 684 username, password, ok := r.BasicAuth()
paddy@145 685 if !ok {
paddy@172 686 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 687 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@145 688 return
paddy@145 689 }
paddy@145 690 profile, err := authenticate(username, password, context)
paddy@145 691 if err != nil {
paddy@145 692 if isAuthError(err) {
paddy@172 693 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 694 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@145 695 } else {
paddy@172 696 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
paddy@172 697 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
paddy@145 698 }
paddy@145 699 return
paddy@145 700 }
paddy@145 701 if !profile.ID.Equal(id) {
paddy@172 702 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 703 encode(w, r, http.StatusForbidden, Response{Errors: errors})
paddy@145 704 return
paddy@145 705 }
paddy@145 706 var req ProfileChange
paddy@145 707 decoder := json.NewDecoder(r.Body)
paddy@145 708 err = decoder.Decode(&req)
paddy@145 709 if err != nil {
paddy@149 710 log.Printf("Error decoding request: %#+v\n", err)
paddy@145 711 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@145 712 return
paddy@145 713 }
paddy@145 714 req.Iterations = nil
paddy@145 715 req.Salt = nil
paddy@145 716 req.PassphraseScheme = nil
paddy@145 717 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
paddy@145 718 req.LockedUntil = nil
paddy@145 719 req.LastSeen = nil
paddy@145 720 if req.Passphrase != nil {
paddy@145 721 if len(*req.Passphrase) < MinPassphraseLength {
paddy@172 722 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"})
paddy@172 723 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 724 return
paddy@145 725 }
paddy@145 726 if len(*req.Passphrase) > MaxPassphraseLength {
paddy@172 727 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"})
paddy@172 728 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 729 return
paddy@145 730 }
paddy@145 731 iterations := context.config.iterations
paddy@145 732 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@145 733 if !ok {
paddy@145 734 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 735 return
paddy@145 736 }
paddy@145 737 curScheme := CurPassphraseScheme
paddy@145 738 req.PassphraseScheme = &curScheme
paddy@145 739 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
paddy@145 740 if err != nil {
paddy@145 741 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 742 return
paddy@145 743 }
paddy@145 744 req.Passphrase = &passphrase
paddy@145 745 req.Salt = &salt
paddy@145 746 req.Iterations = &iterations
paddy@145 747 }
paddy@145 748 if req.PassphraseReset != nil {
paddy@145 749 now := time.Now()
paddy@145 750 req.PassphraseResetCreated = &now
paddy@145 751 }
paddy@145 752 err = req.Validate()
paddy@145 753 if err != nil {
paddy@145 754 var status int
paddy@172 755 var resp Response
paddy@145 756 switch err {
paddy@145 757 case ErrEmptyChange:
paddy@145 758 resp.Profiles = []Profile{profile}
paddy@145 759 status = http.StatusOK
paddy@145 760 default:
paddy@172 761 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
paddy@145 762 resp.Errors = errors
paddy@145 763 status = http.StatusInternalServerError
paddy@145 764 }
paddy@145 765 encode(w, r, status, resp)
paddy@145 766 return
paddy@145 767 }
paddy@145 768 err = context.UpdateProfile(id, req)
paddy@145 769 if err != nil {
paddy@145 770 if err == ErrProfileNotFound {
paddy@172 771 errors = append(errors, RequestError{Slug: RequestErrNotFound})
paddy@172 772 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@145 773 return
paddy@145 774 }
paddy@145 775 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 776 return
paddy@145 777 }
paddy@145 778 profile.ApplyChange(req)
paddy@172 779 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
paddy@145 780 return
paddy@145 781 }
paddy@160 782
paddy@160 783 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 784 errors := []RequestError{}
paddy@160 785 vars := mux.Vars(r)
paddy@160 786 if vars["id"] == "" {
paddy@172 787 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
paddy@172 788 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@160 789 return
paddy@160 790 }
paddy@160 791 id, err := uuid.Parse(vars["id"])
paddy@160 792 if err != nil {
paddy@172 793 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 794 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@160 795 return
paddy@160 796 }
paddy@160 797 username, password, ok := r.BasicAuth()
paddy@160 798 if !ok {
paddy@172 799 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 800 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@160 801 return
paddy@160 802 }
paddy@160 803 profile, err := authenticate(username, password, context)
paddy@160 804 if err != nil {
paddy@160 805 if isAuthError(err) {
paddy@172 806 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 807 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@160 808 } else {
paddy@172 809 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
paddy@172 810 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
paddy@160 811 }
paddy@160 812 return
paddy@160 813 }
paddy@160 814 if !profile.ID.Equal(id) {
paddy@172 815 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 816 encode(w, r, http.StatusForbidden, Response{Errors: errors})
paddy@160 817 return
paddy@160 818 }
paddy@161 819 err = context.DeleteProfile(id)
paddy@160 820 if err != nil {
paddy@160 821 if err == ErrProfileNotFound {
paddy@172 822 errors = append(errors, RequestError{Slug: RequestErrNotFound})
paddy@172 823 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@160 824 return
paddy@160 825 }
paddy@160 826 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@160 827 return
paddy@160 828 }
paddy@172 829 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
paddy@160 830 go cleanUpAfterProfileDeletion(profile.ID, context)
paddy@160 831 }
paddy@172 832
paddy@172 833 func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 834 var errors []RequestError
paddy@172 835 vars := mux.Vars(r)
paddy@172 836 if vars["login"] == "" {
paddy@172 837 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
paddy@172 838 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
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@178 852 // clear verification code so it's not exposed
paddy@178 853 // BUG(paddy): We hsould only hide the verification code if it's not an admin request, but auth isn't set up properly for scopes yet
paddy@178 854 login.Verification = ""
paddy@172 855 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
paddy@172 856 }
paddy@172 857
paddy@172 858 func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 859 var errors []RequestError
paddy@172 860 vars := mux.Vars(r)
paddy@172 861 if vars["login"] == "" {
paddy@172 862 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
paddy@172 863 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 864 return
paddy@172 865 }
paddy@172 866 var req LoginChange
paddy@172 867 decoder := json.NewDecoder(r.Body)
paddy@172 868 err := decoder.Decode(&req)
paddy@172 869 if err != nil {
paddy@172 870 log.Printf("Error decoding request: %#+v\n", err)
paddy@172 871 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@172 872 return
paddy@172 873 }
paddy@172 874 login, err := context.GetLogin(vars["login"])
paddy@172 875 if err != nil {
paddy@172 876 if err == ErrLoginNotFound {
paddy@172 877 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
paddy@172 878 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@172 879 return
paddy@172 880 }
paddy@172 881 log.Printf("Error retrieving login: %#+v\n", err)
paddy@172 882 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@172 883 return
paddy@172 884 }
paddy@172 885 if req.Verification != nil {
paddy@172 886 err = context.VerifyLogin(vars["login"], *req.Verification)
paddy@172 887 if err != nil {
paddy@172 888 if err == ErrLoginNotFound {
paddy@172 889 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
paddy@172 890 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@172 891 return
paddy@172 892 } else if err == ErrLoginVerificationInvalid {
paddy@172 893 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"})
paddy@172 894 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 895 return
paddy@172 896 }
paddy@172 897 log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err)
paddy@172 898 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@172 899 return
paddy@172 900 }
paddy@181 901 go context.SendModelEvent(login, authEvents.ActionLoginVerified)
paddy@172 902 login.Verified = true
paddy@172 903 } else if req.ResendVerification != nil {
paddy@172 904 if !*req.ResendVerification {
paddy@172 905 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"})
paddy@172 906 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 907 return
paddy@172 908 }
paddy@181 909 go context.SendModelEvent(login, authEvents.ActionResendVerification)
paddy@172 910 } else {
paddy@172 911 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
paddy@172 912 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 913 return
paddy@172 914 }
paddy@178 915 // clear the Verification code so it's not exposed
paddy@178 916 login.Verification = ""
paddy@172 917 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
paddy@172 918 }