auth

Paddy 2015-05-12 Parent:c45b946abe78 Child:581c60f8dd23

167:0ff23f3a4ede Go to Latest

auth/profile.go

Implement an endpoint for token information. Implement an endpoint that allows us to look up information on a token. We strip the refresh token before the response is sent to avoid leaking the response token.

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@165 462 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS")
paddy@166 463 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS")
paddy@165 464 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS")
paddy@165 465 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS")
paddy@167 466 // TODO: r.Handle("/profiles/{id}/tokens", wrap(context, ListTokensHandler)).Methods("GET", "OPTIONS")
paddy@128 467 // BUG(paddy): We need to implement a handler that will add a login to a profile.
paddy@128 468 // 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 469 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
paddy@105 470 }
paddy@105 471
paddy@166 472 // GetProfileHandler is an HTTP handler for retrieving a profile.
paddy@166 473 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@166 474 errors := []requestError{}
paddy@166 475 authz := r.Header.Get("Authorization")
paddy@166 476 if !strings.HasPrefix(authz, "Bearer ") {
paddy@166 477 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@166 478 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@166 479 return
paddy@166 480 }
paddy@166 481 authz = strings.TrimPrefix(authz, "Bearer ")
paddy@166 482 vars := mux.Vars(r)
paddy@166 483 if vars["id"] == "" {
paddy@166 484 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@166 485 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@166 486 return
paddy@166 487 }
paddy@166 488 id, err := uuid.Parse(vars["id"])
paddy@166 489 if err != nil {
paddy@166 490 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
paddy@166 491 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@166 492 return
paddy@166 493 }
paddy@166 494 token, err := context.GetToken(authz, false)
paddy@166 495 if err != nil || token.Revoked {
paddy@166 496 if err == ErrTokenNotFound || token.Revoked {
paddy@166 497 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@166 498 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@166 499 return
paddy@166 500 } else {
paddy@166 501 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@166 502 return
paddy@166 503 }
paddy@166 504 }
paddy@166 505 if !id.Equal(token.ProfileID) {
paddy@166 506 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@166 507 encode(w, r, http.StatusForbidden, response{Errors: errors})
paddy@166 508 return
paddy@166 509 }
paddy@166 510 profile, err := context.GetProfileByID(id)
paddy@166 511 if err != nil {
paddy@166 512 if err == ErrProfileNotFound {
paddy@166 513 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
paddy@166 514 encode(w, r, http.StatusNotFound, response{Errors: errors})
paddy@166 515 return
paddy@166 516 }
paddy@166 517 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@166 518 return
paddy@166 519 }
paddy@166 520 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
paddy@166 521 return
paddy@166 522 }
paddy@166 523
paddy@99 524 // CreateProfileHandler is an HTTP handler for registering new profiles.
paddy@99 525 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@99 526 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@99 527 if !ok {
paddy@149 528 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
paddy@105 529 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 530 return
paddy@99 531 }
paddy@99 532 var req newProfileRequest
paddy@99 533 errors := []requestError{}
paddy@99 534 decoder := json.NewDecoder(r.Body)
paddy@99 535 err := decoder.Decode(&req)
paddy@99 536 if err != nil {
paddy@105 537 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@99 538 return
paddy@99 539 }
paddy@99 540 errors = append(errors, validateNewProfileRequest(&req)...)
paddy@99 541 if len(errors) > 0 {
paddy@105 542 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@99 543 return
paddy@99 544 }
paddy@99 545 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
paddy@99 546 if err != nil {
paddy@149 547 log.Printf("Error creating encoded passphrase: %#+v\n", err)
paddy@105 548 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 549 return
paddy@99 550 }
paddy@99 551 profile := Profile{
paddy@99 552 ID: uuid.NewID(),
paddy@99 553 Name: req.Name,
paddy@99 554 Passphrase: string(passphrase),
paddy@99 555 Iterations: context.config.iterations,
paddy@99 556 Salt: string(salt),
paddy@99 557 PassphraseScheme: CurPassphraseScheme,
paddy@99 558 Created: time.Now(),
paddy@99 559 LastSeen: time.Now(),
paddy@99 560 }
paddy@99 561 err = context.SaveProfile(profile)
paddy@99 562 if err != nil {
paddy@105 563 if err == ErrProfileAlreadyExists {
paddy@105 564 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}})
paddy@105 565 return
paddy@105 566 }
paddy@149 567 log.Printf("Error saving profile: %#+v\n", err)
paddy@105 568 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 569 return
paddy@99 570 }
paddy@99 571 logins := []Login{}
paddy@99 572 login := Login{
paddy@99 573 Type: "email",
paddy@99 574 Value: req.Email,
paddy@99 575 Created: profile.Created,
paddy@99 576 LastUsed: profile.Created,
paddy@99 577 ProfileID: profile.ID,
paddy@99 578 }
paddy@99 579 err = context.AddLogin(login)
paddy@99 580 if err != nil {
paddy@105 581 if err == ErrLoginAlreadyExists {
paddy@105 582 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}})
paddy@105 583 return
paddy@105 584 }
paddy@149 585 log.Printf("Error adding login: %#+v\n", err)
paddy@105 586 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 587 return
paddy@99 588 }
paddy@99 589 logins = append(logins, login)
paddy@105 590 resp := response{
paddy@105 591 Logins: logins,
paddy@105 592 Profiles: []Profile{profile},
paddy@105 593 }
paddy@105 594 encode(w, r, http.StatusCreated, resp)
paddy@99 595 // TODO(paddy): should we kick off the email validation flow?
paddy@99 596 }
paddy@145 597
paddy@145 598 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@145 599 errors := []requestError{}
paddy@145 600 vars := mux.Vars(r)
paddy@145 601 if vars["id"] == "" {
paddy@145 602 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@145 603 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@145 604 return
paddy@145 605 }
paddy@145 606 id, err := uuid.Parse(vars["id"])
paddy@145 607 if err != nil {
paddy@145 608 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@145 609 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@145 610 return
paddy@145 611 }
paddy@145 612 username, password, ok := r.BasicAuth()
paddy@145 613 if !ok {
paddy@145 614 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@145 615 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@145 616 return
paddy@145 617 }
paddy@145 618 profile, err := authenticate(username, password, context)
paddy@145 619 if err != nil {
paddy@145 620 if isAuthError(err) {
paddy@145 621 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@145 622 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@145 623 } else {
paddy@145 624 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@145 625 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@145 626 }
paddy@145 627 return
paddy@145 628 }
paddy@145 629 if !profile.ID.Equal(id) {
paddy@145 630 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@145 631 encode(w, r, http.StatusForbidden, response{Errors: errors})
paddy@145 632 return
paddy@145 633 }
paddy@145 634 var req ProfileChange
paddy@145 635 decoder := json.NewDecoder(r.Body)
paddy@145 636 err = decoder.Decode(&req)
paddy@145 637 if err != nil {
paddy@149 638 log.Printf("Error decoding request: %#+v\n", err)
paddy@145 639 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@145 640 return
paddy@145 641 }
paddy@145 642 req.Iterations = nil
paddy@145 643 req.Salt = nil
paddy@145 644 req.PassphraseScheme = nil
paddy@145 645 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
paddy@145 646 req.LockedUntil = nil
paddy@145 647 req.LastSeen = nil
paddy@145 648 if req.Passphrase != nil {
paddy@145 649 if len(*req.Passphrase) < MinPassphraseLength {
paddy@145 650 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/passphrase"})
paddy@145 651 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@145 652 return
paddy@145 653 }
paddy@145 654 if len(*req.Passphrase) > MaxPassphraseLength {
paddy@145 655 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/passphrase"})
paddy@145 656 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@145 657 return
paddy@145 658 }
paddy@145 659 iterations := context.config.iterations
paddy@145 660 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@145 661 if !ok {
paddy@145 662 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 663 return
paddy@145 664 }
paddy@145 665 curScheme := CurPassphraseScheme
paddy@145 666 req.PassphraseScheme = &curScheme
paddy@145 667 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
paddy@145 668 if err != nil {
paddy@145 669 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 670 return
paddy@145 671 }
paddy@145 672 req.Passphrase = &passphrase
paddy@145 673 req.Salt = &salt
paddy@145 674 req.Iterations = &iterations
paddy@145 675 }
paddy@145 676 if req.PassphraseReset != nil {
paddy@145 677 now := time.Now()
paddy@145 678 req.PassphraseResetCreated = &now
paddy@145 679 }
paddy@145 680 err = req.Validate()
paddy@145 681 if err != nil {
paddy@145 682 var status int
paddy@145 683 var resp response
paddy@145 684 switch err {
paddy@145 685 case ErrEmptyChange:
paddy@145 686 resp.Profiles = []Profile{profile}
paddy@145 687 status = http.StatusOK
paddy@145 688 default:
paddy@145 689 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@145 690 resp.Errors = errors
paddy@145 691 status = http.StatusInternalServerError
paddy@145 692 }
paddy@145 693 encode(w, r, status, resp)
paddy@145 694 return
paddy@145 695 }
paddy@145 696 err = context.UpdateProfile(id, req)
paddy@145 697 if err != nil {
paddy@145 698 if err == ErrProfileNotFound {
paddy@145 699 errors = append(errors, requestError{Slug: requestErrNotFound})
paddy@145 700 encode(w, r, http.StatusNotFound, response{Errors: errors})
paddy@145 701 return
paddy@145 702 }
paddy@145 703 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 704 return
paddy@145 705 }
paddy@145 706 profile.ApplyChange(req)
paddy@145 707 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
paddy@145 708 return
paddy@145 709 }
paddy@160 710
paddy@160 711 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@160 712 errors := []requestError{}
paddy@160 713 vars := mux.Vars(r)
paddy@160 714 if vars["id"] == "" {
paddy@160 715 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@160 716 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@160 717 return
paddy@160 718 }
paddy@160 719 id, err := uuid.Parse(vars["id"])
paddy@160 720 if err != nil {
paddy@160 721 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@160 722 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@160 723 return
paddy@160 724 }
paddy@160 725 username, password, ok := r.BasicAuth()
paddy@160 726 if !ok {
paddy@160 727 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@160 728 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@160 729 return
paddy@160 730 }
paddy@160 731 profile, err := authenticate(username, password, context)
paddy@160 732 if err != nil {
paddy@160 733 if isAuthError(err) {
paddy@160 734 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@160 735 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
paddy@160 736 } else {
paddy@160 737 errors = append(errors, requestError{Slug: requestErrActOfGod})
paddy@160 738 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
paddy@160 739 }
paddy@160 740 return
paddy@160 741 }
paddy@160 742 if !profile.ID.Equal(id) {
paddy@160 743 errors = append(errors, requestError{Slug: requestErrAccessDenied})
paddy@160 744 encode(w, r, http.StatusForbidden, response{Errors: errors})
paddy@160 745 return
paddy@160 746 }
paddy@161 747 err = context.DeleteProfile(id)
paddy@160 748 if err != nil {
paddy@160 749 if err == ErrProfileNotFound {
paddy@160 750 errors = append(errors, requestError{Slug: requestErrNotFound})
paddy@160 751 encode(w, r, http.StatusNotFound, response{Errors: errors})
paddy@160 752 return
paddy@160 753 }
paddy@160 754 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@160 755 return
paddy@160 756 }
paddy@160 757 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
paddy@160 758 go cleanUpAfterProfileDeletion(profile.ID, context)
paddy@160 759 }