auth

Paddy 2015-03-22 Parent:8267e1c8bcd1 Child:3223a8e679db

151:77db7c65216c Go to Latest

auth/profile.go

Implement postgres clientStore. Stop requiring the client ID be passed to clientStore.addEndpoints and context.AddEndpoints. The Endpoints themselves contain the client ID. When using the authd server, set the log flags to include the file path and line number. Add an ErrEndpointAlreadyExists error, to return when creating an endpoint and its ID already exists in the database. Add a Deleted property to Clients and remove the clientStore.deleteClient and context.DeleteClient methods. We're not going to actually remove that data, and we want to be able to restore it, so include it in the ClientChange type and call it using UpdateClient. Create a ClientChange.Empty helper method that will return whether the ClientChange has any changes to perform. Return ErrClientNotFound from clientStore.getClient if the Client's Deleted property is set to true. This also requires us to ignore ErrClientNotFound errors when calling memstore.listClientsByOwner, as they should just be skipped instead of returning an error. Add the postgres type methods needed to implement clientStore. Include postgres as a clientStore if the testing.Short() flag is not set. Generate a new ID for the Client on every run in the tests, now that we can't actually remove it from the database/memstore in code. We really just need a *Store.Reset() function that erases all the data and starts over again, to give the tests a clean execution environment (and so they can clean up after themselves). Add the CREATE TABLE statements for the Clients table and the Endpoints table to sql/postgres_init.sql.

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