auth

Paddy 2015-06-29 Parent:b0d1b3e39fc8 Child:5d52b9d83184

176:fc68085eb40d Go to Latest

auth/profile.go

Add kubernetes definitions. Define a replication controller that will spin up authd servers (using Ducky right now--other instances should rename the ducky parts appropriately). Also, my understanding of which labels go where may be shaky, which is probably evidenced by the fact that all of these things share the same lables. _Whatever_. It also hooks the generated pods up to the JWT secret volume, so they can properly read the JWT secret. Also, created a LoadBalancer Service that will route traffic to the pods created by the Replication Controller.

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@172 635 go context.SendLoginVerification(login)
paddy@99 636 }
paddy@145 637
paddy@145 638 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 639 errors := []RequestError{}
paddy@145 640 vars := mux.Vars(r)
paddy@145 641 if vars["id"] == "" {
paddy@172 642 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
paddy@172 643 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 644 return
paddy@145 645 }
paddy@145 646 id, err := uuid.Parse(vars["id"])
paddy@145 647 if err != nil {
paddy@172 648 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 649 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 650 return
paddy@145 651 }
paddy@145 652 username, password, ok := r.BasicAuth()
paddy@145 653 if !ok {
paddy@172 654 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 655 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@145 656 return
paddy@145 657 }
paddy@145 658 profile, err := authenticate(username, password, context)
paddy@145 659 if err != nil {
paddy@145 660 if isAuthError(err) {
paddy@172 661 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 662 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@145 663 } else {
paddy@172 664 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
paddy@172 665 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
paddy@145 666 }
paddy@145 667 return
paddy@145 668 }
paddy@145 669 if !profile.ID.Equal(id) {
paddy@172 670 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 671 encode(w, r, http.StatusForbidden, Response{Errors: errors})
paddy@145 672 return
paddy@145 673 }
paddy@145 674 var req ProfileChange
paddy@145 675 decoder := json.NewDecoder(r.Body)
paddy@145 676 err = decoder.Decode(&req)
paddy@145 677 if err != nil {
paddy@149 678 log.Printf("Error decoding request: %#+v\n", err)
paddy@145 679 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@145 680 return
paddy@145 681 }
paddy@145 682 req.Iterations = nil
paddy@145 683 req.Salt = nil
paddy@145 684 req.PassphraseScheme = nil
paddy@145 685 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
paddy@145 686 req.LockedUntil = nil
paddy@145 687 req.LastSeen = nil
paddy@145 688 if req.Passphrase != nil {
paddy@145 689 if len(*req.Passphrase) < MinPassphraseLength {
paddy@172 690 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"})
paddy@172 691 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 692 return
paddy@145 693 }
paddy@145 694 if len(*req.Passphrase) > MaxPassphraseLength {
paddy@172 695 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"})
paddy@172 696 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 697 return
paddy@145 698 }
paddy@145 699 iterations := context.config.iterations
paddy@145 700 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@145 701 if !ok {
paddy@145 702 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 703 return
paddy@145 704 }
paddy@145 705 curScheme := CurPassphraseScheme
paddy@145 706 req.PassphraseScheme = &curScheme
paddy@145 707 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
paddy@145 708 if err != nil {
paddy@145 709 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 710 return
paddy@145 711 }
paddy@145 712 req.Passphrase = &passphrase
paddy@145 713 req.Salt = &salt
paddy@145 714 req.Iterations = &iterations
paddy@145 715 }
paddy@145 716 if req.PassphraseReset != nil {
paddy@145 717 now := time.Now()
paddy@145 718 req.PassphraseResetCreated = &now
paddy@145 719 }
paddy@145 720 err = req.Validate()
paddy@145 721 if err != nil {
paddy@145 722 var status int
paddy@172 723 var resp Response
paddy@145 724 switch err {
paddy@145 725 case ErrEmptyChange:
paddy@145 726 resp.Profiles = []Profile{profile}
paddy@145 727 status = http.StatusOK
paddy@145 728 default:
paddy@172 729 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
paddy@145 730 resp.Errors = errors
paddy@145 731 status = http.StatusInternalServerError
paddy@145 732 }
paddy@145 733 encode(w, r, status, resp)
paddy@145 734 return
paddy@145 735 }
paddy@145 736 err = context.UpdateProfile(id, req)
paddy@145 737 if err != nil {
paddy@145 738 if err == ErrProfileNotFound {
paddy@172 739 errors = append(errors, RequestError{Slug: RequestErrNotFound})
paddy@172 740 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@145 741 return
paddy@145 742 }
paddy@145 743 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 744 return
paddy@145 745 }
paddy@145 746 profile.ApplyChange(req)
paddy@172 747 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
paddy@145 748 return
paddy@145 749 }
paddy@160 750
paddy@160 751 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 752 errors := []RequestError{}
paddy@160 753 vars := mux.Vars(r)
paddy@160 754 if vars["id"] == "" {
paddy@172 755 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
paddy@172 756 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@160 757 return
paddy@160 758 }
paddy@160 759 id, err := uuid.Parse(vars["id"])
paddy@160 760 if err != nil {
paddy@172 761 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 762 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@160 763 return
paddy@160 764 }
paddy@160 765 username, password, ok := r.BasicAuth()
paddy@160 766 if !ok {
paddy@172 767 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 768 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@160 769 return
paddy@160 770 }
paddy@160 771 profile, err := authenticate(username, password, context)
paddy@160 772 if err != nil {
paddy@160 773 if isAuthError(err) {
paddy@172 774 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 775 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@160 776 } else {
paddy@172 777 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
paddy@172 778 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
paddy@160 779 }
paddy@160 780 return
paddy@160 781 }
paddy@160 782 if !profile.ID.Equal(id) {
paddy@172 783 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 784 encode(w, r, http.StatusForbidden, Response{Errors: errors})
paddy@160 785 return
paddy@160 786 }
paddy@161 787 err = context.DeleteProfile(id)
paddy@160 788 if err != nil {
paddy@160 789 if err == ErrProfileNotFound {
paddy@172 790 errors = append(errors, RequestError{Slug: RequestErrNotFound})
paddy@172 791 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@160 792 return
paddy@160 793 }
paddy@160 794 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@160 795 return
paddy@160 796 }
paddy@172 797 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
paddy@160 798 go cleanUpAfterProfileDeletion(profile.ID, context)
paddy@160 799 }
paddy@172 800
paddy@172 801 func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 802 var errors []RequestError
paddy@172 803 vars := mux.Vars(r)
paddy@172 804 if vars["login"] == "" {
paddy@172 805 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
paddy@172 806 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 807 return
paddy@172 808 }
paddy@172 809 login, err := context.GetLogin(vars["login"])
paddy@172 810 if err != nil {
paddy@172 811 if err == ErrLoginNotFound {
paddy@172 812 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
paddy@172 813 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@172 814 return
paddy@172 815 }
paddy@172 816 log.Printf("Error retrieving login: %#+v\n", err)
paddy@172 817 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@172 818 return
paddy@172 819 }
paddy@172 820 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
paddy@172 821 }
paddy@172 822
paddy@172 823 func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 824 var errors []RequestError
paddy@172 825 vars := mux.Vars(r)
paddy@172 826 if vars["login"] == "" {
paddy@172 827 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
paddy@172 828 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 829 return
paddy@172 830 }
paddy@172 831 var req LoginChange
paddy@172 832 decoder := json.NewDecoder(r.Body)
paddy@172 833 err := decoder.Decode(&req)
paddy@172 834 if err != nil {
paddy@172 835 log.Printf("Error decoding request: %#+v\n", err)
paddy@172 836 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@172 837 return
paddy@172 838 }
paddy@172 839 login, err := context.GetLogin(vars["login"])
paddy@172 840 if err != nil {
paddy@172 841 if err == ErrLoginNotFound {
paddy@172 842 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
paddy@172 843 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@172 844 return
paddy@172 845 }
paddy@172 846 log.Printf("Error retrieving login: %#+v\n", err)
paddy@172 847 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@172 848 return
paddy@172 849 }
paddy@172 850 if req.Verification != nil {
paddy@172 851 err = context.VerifyLogin(vars["login"], *req.Verification)
paddy@172 852 if err != nil {
paddy@172 853 if err == ErrLoginNotFound {
paddy@172 854 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
paddy@172 855 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@172 856 return
paddy@172 857 } else if err == ErrLoginVerificationInvalid {
paddy@172 858 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"})
paddy@172 859 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 860 return
paddy@172 861 }
paddy@172 862 log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err)
paddy@172 863 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@172 864 return
paddy@172 865 }
paddy@172 866 login.Verified = true
paddy@172 867 } else if req.ResendVerification != nil {
paddy@172 868 if !*req.ResendVerification {
paddy@172 869 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"})
paddy@172 870 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 871 return
paddy@172 872 }
paddy@172 873 context.SendLoginVerification(login)
paddy@172 874 } else {
paddy@172 875 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
paddy@172 876 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 877 return
paddy@172 878 }
paddy@172 879 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
paddy@172 880 }