auth

Paddy 2015-04-25 Parent:73e12d5a1124 Child:04c8edf89e3b

164:cf1aef6eb81f Go to Latest

auth/profile.go

Clean up after Client deletion, finish cleaning up after Profile deletion. 6f473576c6ae started cleaning up after Profiles when they're deleted, but didn't clean up the Clients created by that Profile. This fixes that, and also fixes a BUG note about cleaning up after a Client when it's deleted. Extend the authorizationCodeStore to have a deleteAuthorizationCodesByClientID method that will delete the AuthorizationCodes that have been granted by the Client specified by the passed ID. We also implemented this in memstore and postgres, so tests continue to pass. Extend the clientStore to have a deleteClientsByOwner method that will delete the Clients that were created by the Profile specified by the passed ID. We also implemented this in memstore and postgres, so tests continue to pass. Extend the clientStore to have a removeEndpointsByClientID method that will delete the Endpoints that belong(ed) to a the Client specified by the passed ID. We also implemented this in memstore and postgres, so tests continue to pass. Extend the tokenStore to have a revokeTokensByClientID method that will revoke all the Tokens that were granted to the Client specified by the passed ID. We also implemented this in memstore and postgres, so tests continue to pass. When listing Clients by their owner, allow setting the num argument (which controls how many to return) to 0 or lower, and using that to signal "return all Clients belonging to this owner", instead of paging. This is useful when deleting the Clients belonging to a Profile as part of the cleanup after deleting the Profile. Create a cleanUpAfterClientDeletion helper function that will delete the Endpoints and AuthorizationCodes belonging to a Client, and revoke the Tokens belonging to a Client, as part of cleaning up after a Client has been deleted. Add a check in the handler for listing Clients owned by a Profile to disallow the num argument to be lower than 1, because the API should be forced to page. Call our cleanUpAfterClientDeletion once the Client has been deleted in the appropriate handler. Fill out our Context with new methods to wrap all the new methods we're adding to our *Stores. In cleanUpAfterProfileDeletion, obtain a list of clients belonging to the owner, use our new DeleteClientsByOwner method to remove all of them, and then use the list to run our new cleanUpAfterClientDeletion function to clear away the final remnants of a Profile when it's deleted.

History
paddy@27 1 package auth
paddy@27 2
paddy@27 3 import (
paddy@99 4 "encoding/json"
paddy@38 5 "errors"
paddy@149 6 "log"
paddy@99 7 "net/http"
paddy@99 8 "regexp"
paddy@99 9 "strings"
paddy@27 10 "time"
paddy@27 11
paddy@107 12 "code.secondbit.org/uuid.hg"
paddy@105 13 "github.com/gorilla/mux"
paddy@27 14 )
paddy@27 15
paddy@48 16 const (
paddy@57 17 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive.
paddy@48 18 MinPassphraseLength = 6
paddy@57 19 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive.
paddy@48 20 MaxPassphraseLength = 64
paddy@69 21 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme
paddy@69 22 CurPassphraseScheme = 1
paddy@99 23 // MaxNameLength is the maximum length, in bytes, of a name, exclusive.
paddy@99 24 MaxNameLength = 64
paddy@99 25 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
paddy@99 26 MaxEmailLength = 64
paddy@48 27 )
paddy@48 28
paddy@38 29 var (
paddy@57 30 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
paddy@57 31 ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
paddy@57 32 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
paddy@57 33 // the same ID already exists in the profileStore.
paddy@57 34 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
paddy@57 35 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
paddy@57 36 ErrProfileNotFound = errors.New("profile not found in profileStore")
paddy@57 37 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
paddy@57 38 // Type and Value already exists in the profileStore.
paddy@57 39 ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
paddy@57 40 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
paddy@57 41 ErrLoginNotFound = errors.New("login not found in profileStore")
paddy@48 42
paddy@57 43 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
paddy@57 44 // Passphrase, and requires one.
paddy@57 45 ErrMissingPassphrase = errors.New("missing passphrase")
paddy@57 46 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
paddy@57 47 // a PassphraseReset, and requires one.
paddy@57 48 ErrMissingPassphraseReset = errors.New("missing passphrase reset")
paddy@57 49 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
paddy@57 50 // contain a PassphraseResetCreated, and requires one.
paddy@48 51 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
paddy@57 52 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
paddy@57 53 // but the Passphrase is shorter than MinPassphraseLength.
paddy@57 54 ErrPassphraseTooShort = errors.New("passphrase too short")
paddy@57 55 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
paddy@57 56 // but the Passphrase is longer than MaxPassphraseLength.
paddy@57 57 ErrPassphraseTooLong = errors.New("passphrase too long")
paddy@99 58
paddy@99 59 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected
paddy@99 60 // of being compromised.
paddy@99 61 ErrProfileCompromised = errors.New("profile compromised")
paddy@99 62 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain
paddy@99 63 // duration, to prevent brute force attacks.
paddy@99 64 ErrProfileLocked = errors.New("profile locked")
paddy@38 65 )
paddy@38 66
paddy@57 67 // Profile represents a single user of the service,
paddy@158 68 // including their authentication information.
paddy@27 69 type Profile struct {
paddy@105 70 ID uuid.ID `json:"id,omitempty"`
paddy@105 71 Name string `json:"name,omitempty"`
paddy@105 72 Passphrase string `json:"-"`
paddy@105 73 Iterations int `json:"-"`
paddy@105 74 Salt string `json:"-"`
paddy@105 75 PassphraseScheme int `json:"-"`
paddy@105 76 Compromised bool `json:"-"`
paddy@105 77 LockedUntil time.Time `json:"-"`
paddy@105 78 PassphraseReset string `json:"-"`
paddy@105 79 PassphraseResetCreated time.Time `json:"-"`
paddy@105 80 Created time.Time `json:"created,omitempty"`
paddy@105 81 LastSeen time.Time `json:"last_seen,omitempty"`
paddy@38 82 }
paddy@38 83
paddy@57 84 // ApplyChange applies the properties of the passed ProfileChange
paddy@57 85 // to the Profile it is called on.
paddy@38 86 func (p *Profile) ApplyChange(change ProfileChange) {
paddy@38 87 if change.Name != nil {
paddy@38 88 p.Name = *change.Name
paddy@38 89 }
paddy@38 90 if change.Passphrase != nil {
paddy@38 91 p.Passphrase = *change.Passphrase
paddy@38 92 }
paddy@38 93 if change.Iterations != nil {
paddy@38 94 p.Iterations = *change.Iterations
paddy@38 95 }
paddy@38 96 if change.Salt != nil {
paddy@38 97 p.Salt = *change.Salt
paddy@38 98 }
paddy@38 99 if change.PassphraseScheme != nil {
paddy@38 100 p.PassphraseScheme = *change.PassphraseScheme
paddy@38 101 }
paddy@38 102 if change.Compromised != nil {
paddy@38 103 p.Compromised = *change.Compromised
paddy@38 104 }
paddy@38 105 if change.LockedUntil != nil {
paddy@38 106 p.LockedUntil = *change.LockedUntil
paddy@38 107 }
paddy@38 108 if change.PassphraseReset != nil {
paddy@38 109 p.PassphraseReset = *change.PassphraseReset
paddy@38 110 }
paddy@38 111 if change.PassphraseResetCreated != nil {
paddy@38 112 p.PassphraseResetCreated = *change.PassphraseResetCreated
paddy@38 113 }
paddy@38 114 if change.LastSeen != nil {
paddy@38 115 p.LastSeen = *change.LastSeen
paddy@38 116 }
paddy@38 117 }
paddy@38 118
paddy@57 119 // ApplyBulkChange applies the properties of the passed BulkProfileChange
paddy@57 120 // to the Profile it is called on.
paddy@44 121 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
paddy@44 122 if change.Compromised != nil {
paddy@44 123 p.Compromised = *change.Compromised
paddy@44 124 }
paddy@44 125 }
paddy@44 126
paddy@57 127 // ProfileChange represents a single atomic change to a Profile's mutable data.
paddy@38 128 type ProfileChange struct {
paddy@38 129 Name *string
paddy@38 130 Passphrase *string
paddy@69 131 Iterations *int
paddy@38 132 Salt *string
paddy@38 133 PassphraseScheme *int
paddy@38 134 Compromised *bool
paddy@38 135 LockedUntil *time.Time
paddy@38 136 PassphraseReset *string
paddy@38 137 PassphraseResetCreated *time.Time
paddy@38 138 LastSeen *time.Time
paddy@38 139 }
paddy@38 140
paddy@149 141 func (c ProfileChange) Empty() bool {
paddy@161 142 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 143 }
paddy@149 144
paddy@57 145 // Validate checks the ProfileChange it is called on
paddy@57 146 // and asserts its internal validity, or lack thereof.
paddy@57 147 // A descriptive error will be returned in the case of
paddy@57 148 // an invalid change.
paddy@38 149 func (c ProfileChange) Validate() error {
paddy@149 150 if c.Empty() {
paddy@48 151 return ErrEmptyChange
paddy@48 152 }
paddy@48 153 if c.PassphraseScheme != nil && c.Passphrase == nil {
paddy@48 154 return ErrMissingPassphrase
paddy@48 155 }
paddy@48 156 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
paddy@48 157 return ErrMissingPassphraseResetCreated
paddy@48 158 }
paddy@48 159 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
paddy@48 160 return ErrMissingPassphraseReset
paddy@48 161 }
paddy@48 162 if c.Salt != nil && c.Passphrase == nil {
paddy@48 163 return ErrMissingPassphrase
paddy@48 164 }
paddy@48 165 if c.Iterations != nil && c.Passphrase == nil {
paddy@48 166 return ErrMissingPassphrase
paddy@48 167 }
paddy@38 168 return nil
paddy@27 169 }
paddy@27 170
paddy@57 171 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
paddy@57 172 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
paddy@57 173 // ProfileChange values across many Profiles all at once.
paddy@44 174 type BulkProfileChange struct {
paddy@44 175 Compromised *bool
paddy@44 176 }
paddy@44 177
paddy@149 178 func (b BulkProfileChange) Empty() bool {
paddy@149 179 return b.Compromised == nil
paddy@149 180 }
paddy@149 181
paddy@57 182 // Validate checks the BulkProfileChange it is called on
paddy@57 183 // and asserts its internal validity, or lack thereof.
paddy@57 184 // A descriptive error will be returned in the case of an
paddy@57 185 // invalid change.
paddy@44 186 func (b BulkProfileChange) Validate() error {
paddy@149 187 if b.Empty() {
paddy@48 188 return ErrEmptyChange
paddy@48 189 }
paddy@44 190 return nil
paddy@44 191 }
paddy@44 192
paddy@57 193 // Login represents a single human-friendly identifier for
paddy@57 194 // a given Profile that can be used to log into that Profile.
paddy@57 195 // Each Profile may only have one Login for each Type.
paddy@27 196 type Login struct {
paddy@105 197 Type string `json:"type,omitempty"`
paddy@105 198 Value string `json:"value,omitempty"`
paddy@105 199 ProfileID uuid.ID `json:"profile_id,omitempty"`
paddy@105 200 Created time.Time `json:"created,omitempty"`
paddy@105 201 LastUsed time.Time `json:"last_used,omitempty"`
paddy@27 202 }
paddy@27 203
paddy@99 204 type newProfileRequest struct {
paddy@99 205 Email string `json:"email"`
paddy@99 206 Passphrase string `json:"passphrase"`
paddy@99 207 Name string `json:"name"`
paddy@99 208 }
paddy@99 209
paddy@99 210 func validateNewProfileRequest(req *newProfileRequest) []requestError {
paddy@99 211 errors := []requestError{}
paddy@99 212 req.Name = strings.TrimSpace(req.Name)
paddy@99 213 req.Email = strings.TrimSpace(req.Email)
paddy@99 214 if len(req.Passphrase) < MinPassphraseLength {
paddy@99 215 errors = append(errors, requestError{
paddy@99 216 Slug: requestErrInsufficient,
paddy@99 217 Field: "/passphrase",
paddy@99 218 })
paddy@99 219 }
paddy@99 220 if len(req.Passphrase) > MaxPassphraseLength {
paddy@99 221 errors = append(errors, requestError{
paddy@99 222 Slug: requestErrOverflow,
paddy@99 223 Field: "/passphrase",
paddy@99 224 })
paddy@99 225 }
paddy@99 226 if len(req.Name) > MaxNameLength {
paddy@99 227 errors = append(errors, requestError{
paddy@99 228 Slug: requestErrOverflow,
paddy@99 229 Field: "/name",
paddy@99 230 })
paddy@99 231 }
paddy@99 232 if req.Email == "" {
paddy@99 233 errors = append(errors, requestError{
paddy@99 234 Slug: requestErrMissing,
paddy@99 235 Field: "/email",
paddy@99 236 })
paddy@99 237 }
paddy@99 238 if len(req.Email) > MaxEmailLength {
paddy@99 239 errors = append(errors, requestError{
paddy@99 240 Slug: requestErrOverflow,
paddy@99 241 Field: "/email",
paddy@99 242 })
paddy@99 243 }
paddy@99 244 re := regexp.MustCompile(".+@.+\\..+")
paddy@99 245 if !re.Match([]byte(req.Email)) {
paddy@99 246 errors = append(errors, requestError{
paddy@105 247 Slug: requestErrInvalidFormat,
paddy@99 248 Field: "/email",
paddy@99 249 })
paddy@99 250 }
paddy@99 251 return errors
paddy@99 252 }
paddy@99 253
paddy@57 254 type profileStore interface {
paddy@57 255 getProfileByID(id uuid.ID) (Profile, error)
paddy@69 256 getProfileByLogin(value string) (Profile, error)
paddy@57 257 saveProfile(profile Profile) error
paddy@57 258 updateProfile(id uuid.ID, change ProfileChange) error
paddy@57 259 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
paddy@161 260 deleteProfile(id uuid.ID) error
paddy@44 261
paddy@57 262 addLogin(login Login) error
paddy@69 263 removeLogin(value string, profile uuid.ID) error
paddy@160 264 removeLoginsByProfile(profile uuid.ID) error
paddy@69 265 recordLoginUse(value string, when time.Time) error
paddy@57 266 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
paddy@38 267 }
paddy@27 268
paddy@57 269 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
paddy@38 270 m.profileLock.RLock()
paddy@38 271 defer m.profileLock.RUnlock()
paddy@38 272 p, ok := m.profiles[id.String()]
paddy@38 273 if !ok {
paddy@38 274 return Profile{}, ErrProfileNotFound
paddy@38 275 }
paddy@38 276 return p, nil
paddy@27 277 }
paddy@38 278
paddy@69 279 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
paddy@44 280 m.loginLock.RLock()
paddy@44 281 defer m.loginLock.RUnlock()
paddy@69 282 login, ok := m.logins[value]
paddy@44 283 if !ok {
paddy@44 284 return Profile{}, ErrLoginNotFound
paddy@44 285 }
paddy@44 286 m.profileLock.RLock()
paddy@44 287 defer m.profileLock.RUnlock()
paddy@44 288 profile, ok := m.profiles[login.ProfileID.String()]
paddy@44 289 if !ok {
paddy@44 290 return Profile{}, ErrProfileNotFound
paddy@44 291 }
paddy@44 292 return profile, nil
paddy@38 293 }
paddy@38 294
paddy@57 295 func (m *memstore) saveProfile(profile Profile) error {
paddy@38 296 m.profileLock.Lock()
paddy@38 297 defer m.profileLock.Unlock()
paddy@38 298 _, ok := m.profiles[profile.ID.String()]
paddy@38 299 if ok {
paddy@38 300 return ErrProfileAlreadyExists
paddy@38 301 }
paddy@38 302 m.profiles[profile.ID.String()] = profile
paddy@38 303 return nil
paddy@38 304 }
paddy@38 305
paddy@57 306 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
paddy@38 307 m.profileLock.Lock()
paddy@38 308 defer m.profileLock.Unlock()
paddy@38 309 p, ok := m.profiles[id.String()]
paddy@38 310 if !ok {
paddy@38 311 return ErrProfileNotFound
paddy@38 312 }
paddy@38 313 p.ApplyChange(change)
paddy@38 314 m.profiles[id.String()] = p
paddy@38 315 return nil
paddy@38 316 }
paddy@38 317
paddy@57 318 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
paddy@44 319 m.profileLock.Lock()
paddy@44 320 defer m.profileLock.Unlock()
paddy@44 321 for id, profile := range m.profiles {
paddy@44 322 for _, i := range ids {
paddy@44 323 if id == i.String() {
paddy@44 324 profile.ApplyBulkChange(change)
paddy@44 325 m.profiles[id] = profile
paddy@44 326 break
paddy@44 327 }
paddy@44 328 }
paddy@44 329 }
paddy@44 330 return nil
paddy@44 331 }
paddy@44 332
paddy@161 333 func (m *memstore) deleteProfile(id uuid.ID) error {
paddy@161 334 m.profileLock.Lock()
paddy@161 335 defer m.profileLock.Unlock()
paddy@161 336 if _, ok := m.profiles[id.String()]; !ok {
paddy@161 337 return ErrProfileNotFound
paddy@161 338 }
paddy@161 339 delete(m.profiles, id.String())
paddy@161 340 return nil
paddy@161 341 }
paddy@161 342
paddy@57 343 func (m *memstore) addLogin(login Login) error {
paddy@44 344 m.loginLock.Lock()
paddy@44 345 defer m.loginLock.Unlock()
paddy@69 346 _, ok := m.logins[login.Value]
paddy@44 347 if ok {
paddy@44 348 return ErrLoginAlreadyExists
paddy@44 349 }
paddy@69 350 m.logins[login.Value] = login
paddy@69 351 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
paddy@44 352 return nil
paddy@44 353 }
paddy@44 354
paddy@69 355 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
paddy@44 356 m.loginLock.Lock()
paddy@44 357 defer m.loginLock.Unlock()
paddy@69 358 l, ok := m.logins[value]
paddy@44 359 if !ok {
paddy@44 360 return ErrLoginNotFound
paddy@44 361 }
paddy@44 362 if !l.ProfileID.Equal(profile) {
paddy@44 363 return ErrLoginNotFound
paddy@44 364 }
paddy@69 365 delete(m.logins, value)
paddy@44 366 pos := -1
paddy@44 367 for p, id := range m.profileLoginLookup[profile.String()] {
paddy@69 368 if id == value {
paddy@44 369 pos = p
paddy@44 370 break
paddy@44 371 }
paddy@44 372 }
paddy@44 373 if pos >= 0 {
paddy@44 374 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
paddy@44 375 }
paddy@44 376 return nil
paddy@44 377 }
paddy@44 378
paddy@160 379 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
paddy@160 380 m.loginLock.Lock()
paddy@160 381 defer m.loginLock.Unlock()
paddy@160 382 logins, ok := m.profileLoginLookup[profile.String()]
paddy@160 383 if !ok {
paddy@160 384 return ErrProfileNotFound
paddy@160 385 }
paddy@160 386 delete(m.profileLoginLookup, profile.String())
paddy@160 387 for _, login := range logins {
paddy@160 388 delete(m.logins, login)
paddy@160 389 }
paddy@160 390 return nil
paddy@160 391 }
paddy@160 392
paddy@69 393 func (m *memstore) recordLoginUse(value string, when time.Time) error {
paddy@44 394 m.loginLock.Lock()
paddy@44 395 defer m.loginLock.Unlock()
paddy@69 396 l, ok := m.logins[value]
paddy@44 397 if !ok {
paddy@44 398 return ErrLoginNotFound
paddy@44 399 }
paddy@44 400 l.LastUsed = when
paddy@69 401 m.logins[value] = l
paddy@44 402 return nil
paddy@44 403 }
paddy@44 404
paddy@57 405 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
paddy@44 406 m.loginLock.RLock()
paddy@44 407 defer m.loginLock.RUnlock()
paddy@44 408 ids, ok := m.profileLoginLookup[profile.String()]
paddy@44 409 if !ok {
paddy@44 410 return []Login{}, nil
paddy@44 411 }
paddy@44 412 if len(ids) > num+offset {
paddy@44 413 ids = ids[offset : num+offset]
paddy@44 414 } else if len(ids) > offset {
paddy@44 415 ids = ids[offset:]
paddy@44 416 } else {
paddy@44 417 return []Login{}, nil
paddy@44 418 }
paddy@44 419 logins := []Login{}
paddy@44 420 for _, id := range ids {
paddy@44 421 login, ok := m.logins[id]
paddy@44 422 if !ok {
paddy@44 423 continue
paddy@44 424 }
paddy@44 425 logins = append(logins, login)
paddy@44 426 }
paddy@44 427 return logins, nil
paddy@44 428 }
paddy@99 429
paddy@160 430 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
paddy@160 431 err := context.RemoveLoginsByProfile(profile)
paddy@160 432 if err != nil {
paddy@160 433 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
paddy@160 434 }
paddy@162 435 err = context.TerminateSessionsByProfile(profile)
paddy@162 436 if err != nil {
paddy@162 437 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
paddy@162 438 }
paddy@162 439 err = context.RevokeTokensByProfileID(profile)
paddy@162 440 if err != nil {
paddy@162 441 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
paddy@162 442 }
paddy@163 443 err = context.DeleteAuthorizationCodesByProfileID(profile)
paddy@163 444 if err != nil {
paddy@163 445 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
paddy@163 446 }
paddy@164 447 clients, err := context.ListClientsByOwner(profile, -1, 0)
paddy@164 448 if err != nil {
paddy@164 449 log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
paddy@164 450 }
paddy@164 451 err = context.DeleteClientsByOwner(profile)
paddy@164 452 if err != nil {
paddy@164 453 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
paddy@164 454 }
paddy@164 455 for _, client := range clients {
paddy@164 456 cleanUpAfterClientDeletion(client.ID, context)
paddy@164 457 }
paddy@160 458 }
paddy@160 459
paddy@105 460 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
paddy@105 461 func RegisterProfileHandlers(r *mux.Router, context Context) {
paddy@105 462 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST")
paddy@128 463 // BUG(paddy): We need to implement a handler that will return information about a profile or set of profiles.
paddy@145 464 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH")
paddy@160 465 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE")
paddy@128 466 // BUG(paddy): We need to implement a handler that will add a login to a profile.
paddy@128 467 // 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 468 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
paddy@105 469 }
paddy@105 470
paddy@99 471 // CreateProfileHandler is an HTTP handler for registering new profiles.
paddy@99 472 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@99 473 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@99 474 if !ok {
paddy@149 475 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
paddy@105 476 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 477 return
paddy@99 478 }
paddy@99 479 var req newProfileRequest
paddy@99 480 errors := []requestError{}
paddy@99 481 decoder := json.NewDecoder(r.Body)
paddy@99 482 err := decoder.Decode(&req)
paddy@99 483 if err != nil {
paddy@105 484 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@99 485 return
paddy@99 486 }
paddy@99 487 errors = append(errors, validateNewProfileRequest(&req)...)
paddy@99 488 if len(errors) > 0 {
paddy@105 489 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@99 490 return
paddy@99 491 }
paddy@99 492 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
paddy@99 493 if err != nil {
paddy@149 494 log.Printf("Error creating encoded passphrase: %#+v\n", err)
paddy@105 495 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 496 return
paddy@99 497 }
paddy@99 498 profile := Profile{
paddy@99 499 ID: uuid.NewID(),
paddy@99 500 Name: req.Name,
paddy@99 501 Passphrase: string(passphrase),
paddy@99 502 Iterations: context.config.iterations,
paddy@99 503 Salt: string(salt),
paddy@99 504 PassphraseScheme: CurPassphraseScheme,
paddy@99 505 Created: time.Now(),
paddy@99 506 LastSeen: time.Now(),
paddy@99 507 }
paddy@99 508 err = context.SaveProfile(profile)
paddy@99 509 if err != nil {
paddy@105 510 if err == ErrProfileAlreadyExists {
paddy@105 511 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}})
paddy@105 512 return
paddy@105 513 }
paddy@149 514 log.Printf("Error saving profile: %#+v\n", err)
paddy@105 515 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 516 return
paddy@99 517 }
paddy@99 518 logins := []Login{}
paddy@99 519 login := Login{
paddy@99 520 Type: "email",
paddy@99 521 Value: req.Email,
paddy@99 522 Created: profile.Created,
paddy@99 523 LastUsed: profile.Created,
paddy@99 524 ProfileID: profile.ID,
paddy@99 525 }
paddy@99 526 err = context.AddLogin(login)
paddy@99 527 if err != nil {
paddy@105 528 if err == ErrLoginAlreadyExists {
paddy@105 529 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}})
paddy@105 530 return
paddy@105 531 }
paddy@149 532 log.Printf("Error adding login: %#+v\n", err)
paddy@105 533 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 534 return
paddy@99 535 }
paddy@99 536 logins = append(logins, login)
paddy@105 537 resp := response{
paddy@105 538 Logins: logins,
paddy@105 539 Profiles: []Profile{profile},
paddy@105 540 }
paddy@105 541 encode(w, r, http.StatusCreated, resp)
paddy@99 542 // TODO(paddy): should we kick off the email validation flow?
paddy@99 543 }
paddy@145 544
paddy@145 545 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@145 546 errors := []requestError{}
paddy@145 547 vars := mux.Vars(r)
paddy@145 548 if vars["id"] == "" {
paddy@145 549 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@145 550 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@145 551 return
paddy@145 552 }
paddy@145 553 id, err := uuid.Parse(vars["id"])
paddy@145 554 if err != nil {
paddy@145 555 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@145 556 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@145 557 return
paddy@145 558 }
paddy@145 559 username, password, ok := r.BasicAuth()
paddy@145 560 if !ok {
paddy@145 561 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@145 562 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@145 563 return
paddy@145 564 }
paddy@145 565 profile, err := authenticate(username, password, context)
paddy@145 566 if err != nil {
paddy@145 567 if isAuthError(err) {
paddy@145 568 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@145 569 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@145 570 } else {
paddy@145 571 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@145 572 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@145 573 }
paddy@145 574 return
paddy@145 575 }
paddy@145 576 if !profile.ID.Equal(id) {
paddy@145 577 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@145 578 encode(w, r, http.StatusForbidden, response{Errors: errors})
paddy@145 579 return
paddy@145 580 }
paddy@145 581 var req ProfileChange
paddy@145 582 decoder := json.NewDecoder(r.Body)
paddy@145 583 err = decoder.Decode(&req)
paddy@145 584 if err != nil {
paddy@149 585 log.Printf("Error decoding request: %#+v\n", err)
paddy@145 586 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@145 587 return
paddy@145 588 }
paddy@145 589 req.Iterations = nil
paddy@145 590 req.Salt = nil
paddy@145 591 req.PassphraseScheme = nil
paddy@145 592 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
paddy@145 593 req.LockedUntil = nil
paddy@145 594 req.LastSeen = nil
paddy@145 595 if req.Passphrase != nil {
paddy@145 596 if len(*req.Passphrase) < MinPassphraseLength {
paddy@145 597 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/passphrase"})
paddy@145 598 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@145 599 return
paddy@145 600 }
paddy@145 601 if len(*req.Passphrase) > MaxPassphraseLength {
paddy@145 602 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/passphrase"})
paddy@145 603 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@145 604 return
paddy@145 605 }
paddy@145 606 iterations := context.config.iterations
paddy@145 607 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@145 608 if !ok {
paddy@145 609 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 610 return
paddy@145 611 }
paddy@145 612 curScheme := CurPassphraseScheme
paddy@145 613 req.PassphraseScheme = &curScheme
paddy@145 614 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
paddy@145 615 if err != nil {
paddy@145 616 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 617 return
paddy@145 618 }
paddy@145 619 req.Passphrase = &passphrase
paddy@145 620 req.Salt = &salt
paddy@145 621 req.Iterations = &iterations
paddy@145 622 }
paddy@145 623 if req.PassphraseReset != nil {
paddy@145 624 now := time.Now()
paddy@145 625 req.PassphraseResetCreated = &now
paddy@145 626 }
paddy@145 627 err = req.Validate()
paddy@145 628 if err != nil {
paddy@145 629 var status int
paddy@145 630 var resp response
paddy@145 631 switch err {
paddy@145 632 case ErrEmptyChange:
paddy@145 633 resp.Profiles = []Profile{profile}
paddy@145 634 status = http.StatusOK
paddy@145 635 default:
paddy@145 636 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@145 637 resp.Errors = errors
paddy@145 638 status = http.StatusInternalServerError
paddy@145 639 }
paddy@145 640 encode(w, r, status, resp)
paddy@145 641 return
paddy@145 642 }
paddy@145 643 err = context.UpdateProfile(id, req)
paddy@145 644 if err != nil {
paddy@145 645 if err == ErrProfileNotFound {
paddy@145 646 errors = append(errors, requestError{Slug: requestErrNotFound})
paddy@145 647 encode(w, r, http.StatusNotFound, response{Errors: errors})
paddy@145 648 return
paddy@145 649 }
paddy@145 650 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 651 return
paddy@145 652 }
paddy@145 653 profile.ApplyChange(req)
paddy@145 654 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
paddy@145 655 return
paddy@145 656 }
paddy@160 657
paddy@160 658 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@160 659 errors := []requestError{}
paddy@160 660 vars := mux.Vars(r)
paddy@160 661 if vars["id"] == "" {
paddy@160 662 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@160 663 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@160 664 return
paddy@160 665 }
paddy@160 666 id, err := uuid.Parse(vars["id"])
paddy@160 667 if err != nil {
paddy@160 668 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@160 669 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@160 670 return
paddy@160 671 }
paddy@160 672 username, password, ok := r.BasicAuth()
paddy@160 673 if !ok {
paddy@160 674 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@160 675 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@160 676 return
paddy@160 677 }
paddy@160 678 profile, err := authenticate(username, password, context)
paddy@160 679 if err != nil {
paddy@160 680 if isAuthError(err) {
paddy@160 681 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@160 682 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@160 683 } else {
paddy@160 684 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@160 685 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@160 686 }
paddy@160 687 return
paddy@160 688 }
paddy@160 689 if !profile.ID.Equal(id) {
paddy@160 690 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@160 691 encode(w, r, http.StatusForbidden, response{Errors: errors})
paddy@160 692 return
paddy@160 693 }
paddy@161 694 err = context.DeleteProfile(id)
paddy@160 695 if err != nil {
paddy@160 696 if err == ErrProfileNotFound {
paddy@160 697 errors = append(errors, requestError{Slug: requestErrNotFound})
paddy@160 698 encode(w, r, http.StatusNotFound, response{Errors: errors})
paddy@160 699 return
paddy@160 700 }
paddy@160 701 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@160 702 return
paddy@160 703 }
paddy@160 704 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
paddy@160 705 go cleanUpAfterProfileDeletion(profile.ID, context)
paddy@160 706 }