auth

Paddy 2015-12-14 Parent:7bba108d2d9a

181:b7e685839a1b Go to Latest

auth/profile.go

Break out scopes and events. This repo has gotten unwieldy, and there are portions of it that need to be imported by a large number of other packages. For example, scopes will be used in almost every API we write. Rather than importing the entirety of this codebase into every API we write, I've opted to move the scope logic out into a scopes package, with a subpackage for the defined types, which is all most projects actually want to import. We also define some event type constants, and importing those shouldn't require a project to import all our dependencies, either. So I made an events subpackage that just holds those constants. This package has become a little bit of a red-headed stepchild and is do for a refactor, but I'm trying to put that off as long as I can. The refactoring of our scopes stuff has left a bug wherein a token can be granted for scopes that don't exist. I'm going to need to revisit that, and also how to limit scopes to only be granted to the users that should be able to request them. But that's a battle for another day.

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 }