auth
auth/profile.go
Send events when logins are verified. Add an ActionLoginVerified constant to use as the action when a login has been verified. On second thought, this should probably just be "verified", huh? Then we can reuse it across models. Oops. We also added a call to send a login verified event to NSQ when the login is verified.
| 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@178 | 12 "code.secondbit.org/events.hg" |
| paddy@107 | 13 "code.secondbit.org/uuid.hg" |
| paddy@178 | 14 |
| paddy@105 | 15 "github.com/gorilla/mux" |
| paddy@27 | 16 ) |
| paddy@27 | 17 |
| paddy@48 | 18 const ( |
| paddy@57 | 19 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive. |
| paddy@48 | 20 MinPassphraseLength = 6 |
| paddy@57 | 21 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive. |
| paddy@48 | 22 MaxPassphraseLength = 64 |
| paddy@69 | 23 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme |
| paddy@69 | 24 CurPassphraseScheme = 1 |
| paddy@99 | 25 // MaxNameLength is the maximum length, in bytes, of a name, exclusive. |
| paddy@99 | 26 MaxNameLength = 64 |
| paddy@99 | 27 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive. |
| paddy@99 | 28 MaxEmailLength = 64 |
| paddy@178 | 29 |
| paddy@178 | 30 // ActionResendVerification is the action property of the event sent when the user wants to resend their login verification code. |
| paddy@178 | 31 ActionResendVerification = "resend_verification" |
| paddy@179 | 32 ActionLoginVerified = "login_verified" |
| paddy@48 | 33 ) |
| paddy@48 | 34 |
| paddy@38 | 35 var ( |
| paddy@57 | 36 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first. |
| paddy@57 | 37 ErrNoProfileStore = errors.New("no profileStore was specified for the Context") |
| paddy@57 | 38 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with |
| paddy@57 | 39 // the same ID already exists in the profileStore. |
| paddy@57 | 40 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore") |
| paddy@57 | 41 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore. |
| paddy@57 | 42 ErrProfileNotFound = errors.New("profile not found in profileStore") |
| paddy@57 | 43 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same |
| paddy@57 | 44 // Type and Value already exists in the profileStore. |
| paddy@57 | 45 ErrLoginAlreadyExists = errors.New("login already exists in profileStore") |
| paddy@57 | 46 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore. |
| paddy@57 | 47 ErrLoginNotFound = errors.New("login not found in profileStore") |
| paddy@172 | 48 // ErrLoginVerificationInvalid is returned when a Login is verified with the wrong verification code. |
| paddy@172 | 49 ErrLoginVerificationInvalid = errors.New("login verification code incorrect") |
| paddy@48 | 50 |
| paddy@57 | 51 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a |
| paddy@57 | 52 // Passphrase, and requires one. |
| paddy@57 | 53 ErrMissingPassphrase = errors.New("missing passphrase") |
| paddy@57 | 54 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain |
| paddy@57 | 55 // a PassphraseReset, and requires one. |
| paddy@57 | 56 ErrMissingPassphraseReset = errors.New("missing passphrase reset") |
| paddy@57 | 57 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not |
| paddy@57 | 58 // contain a PassphraseResetCreated, and requires one. |
| paddy@48 | 59 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp") |
| paddy@57 | 60 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase, |
| paddy@57 | 61 // but the Passphrase is shorter than MinPassphraseLength. |
| paddy@57 | 62 ErrPassphraseTooShort = errors.New("passphrase too short") |
| paddy@57 | 63 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase, |
| paddy@57 | 64 // but the Passphrase is longer than MaxPassphraseLength. |
| paddy@57 | 65 ErrPassphraseTooLong = errors.New("passphrase too long") |
| paddy@99 | 66 |
| paddy@99 | 67 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected |
| paddy@99 | 68 // of being compromised. |
| paddy@99 | 69 ErrProfileCompromised = errors.New("profile compromised") |
| paddy@99 | 70 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain |
| paddy@99 | 71 // duration, to prevent brute force attacks. |
| paddy@99 | 72 ErrProfileLocked = errors.New("profile locked") |
| paddy@173 | 73 |
| paddy@173 | 74 ScopeLoginAdmin = Scope{ID: "login_admin", Name: "Administer Logins", Description: "Read and write logins, bypassing ACL."} |
| paddy@38 | 75 ) |
| paddy@38 | 76 |
| paddy@57 | 77 // Profile represents a single user of the service, |
| paddy@158 | 78 // including their authentication information. |
| paddy@27 | 79 type Profile struct { |
| paddy@105 | 80 ID uuid.ID `json:"id,omitempty"` |
| paddy@105 | 81 Name string `json:"name,omitempty"` |
| paddy@105 | 82 Passphrase string `json:"-"` |
| paddy@105 | 83 Iterations int `json:"-"` |
| paddy@105 | 84 Salt string `json:"-"` |
| paddy@105 | 85 PassphraseScheme int `json:"-"` |
| paddy@105 | 86 Compromised bool `json:"-"` |
| paddy@105 | 87 LockedUntil time.Time `json:"-"` |
| paddy@105 | 88 PassphraseReset string `json:"-"` |
| paddy@105 | 89 PassphraseResetCreated time.Time `json:"-"` |
| paddy@105 | 90 Created time.Time `json:"created,omitempty"` |
| paddy@105 | 91 LastSeen time.Time `json:"last_seen,omitempty"` |
| paddy@38 | 92 } |
| paddy@38 | 93 |
| paddy@178 | 94 func (p Profile) GetModelName() string { |
| paddy@178 | 95 return "profiles" |
| paddy@178 | 96 } |
| paddy@178 | 97 |
| paddy@178 | 98 func (p Profile) GetID() string { |
| paddy@178 | 99 return p.ID.String() |
| paddy@178 | 100 } |
| paddy@178 | 101 |
| paddy@178 | 102 func (p Profile) GetSystem() string { |
| paddy@178 | 103 return "code.secondbit.org/auth" |
| paddy@178 | 104 } |
| paddy@178 | 105 |
| paddy@57 | 106 // ApplyChange applies the properties of the passed ProfileChange |
| paddy@57 | 107 // to the Profile it is called on. |
| paddy@38 | 108 func (p *Profile) ApplyChange(change ProfileChange) { |
| paddy@38 | 109 if change.Name != nil { |
| paddy@38 | 110 p.Name = *change.Name |
| paddy@38 | 111 } |
| paddy@38 | 112 if change.Passphrase != nil { |
| paddy@38 | 113 p.Passphrase = *change.Passphrase |
| paddy@38 | 114 } |
| paddy@38 | 115 if change.Iterations != nil { |
| paddy@38 | 116 p.Iterations = *change.Iterations |
| paddy@38 | 117 } |
| paddy@38 | 118 if change.Salt != nil { |
| paddy@38 | 119 p.Salt = *change.Salt |
| paddy@38 | 120 } |
| paddy@38 | 121 if change.PassphraseScheme != nil { |
| paddy@38 | 122 p.PassphraseScheme = *change.PassphraseScheme |
| paddy@38 | 123 } |
| paddy@38 | 124 if change.Compromised != nil { |
| paddy@38 | 125 p.Compromised = *change.Compromised |
| paddy@38 | 126 } |
| paddy@38 | 127 if change.LockedUntil != nil { |
| paddy@38 | 128 p.LockedUntil = *change.LockedUntil |
| paddy@38 | 129 } |
| paddy@38 | 130 if change.PassphraseReset != nil { |
| paddy@38 | 131 p.PassphraseReset = *change.PassphraseReset |
| paddy@38 | 132 } |
| paddy@38 | 133 if change.PassphraseResetCreated != nil { |
| paddy@38 | 134 p.PassphraseResetCreated = *change.PassphraseResetCreated |
| paddy@38 | 135 } |
| paddy@38 | 136 if change.LastSeen != nil { |
| paddy@38 | 137 p.LastSeen = *change.LastSeen |
| paddy@38 | 138 } |
| paddy@38 | 139 } |
| paddy@38 | 140 |
| paddy@57 | 141 // ApplyBulkChange applies the properties of the passed BulkProfileChange |
| paddy@57 | 142 // to the Profile it is called on. |
| paddy@44 | 143 func (p *Profile) ApplyBulkChange(change BulkProfileChange) { |
| paddy@44 | 144 if change.Compromised != nil { |
| paddy@44 | 145 p.Compromised = *change.Compromised |
| paddy@44 | 146 } |
| paddy@44 | 147 } |
| paddy@44 | 148 |
| paddy@57 | 149 // ProfileChange represents a single atomic change to a Profile's mutable data. |
| paddy@38 | 150 type ProfileChange struct { |
| paddy@38 | 151 Name *string |
| paddy@38 | 152 Passphrase *string |
| paddy@69 | 153 Iterations *int |
| paddy@38 | 154 Salt *string |
| paddy@38 | 155 PassphraseScheme *int |
| paddy@38 | 156 Compromised *bool |
| paddy@38 | 157 LockedUntil *time.Time |
| paddy@38 | 158 PassphraseReset *string |
| paddy@38 | 159 PassphraseResetCreated *time.Time |
| paddy@38 | 160 LastSeen *time.Time |
| paddy@38 | 161 } |
| paddy@38 | 162 |
| paddy@149 | 163 func (c ProfileChange) Empty() bool { |
| paddy@161 | 164 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 | 165 } |
| paddy@149 | 166 |
| paddy@57 | 167 // Validate checks the ProfileChange it is called on |
| paddy@57 | 168 // and asserts its internal validity, or lack thereof. |
| paddy@57 | 169 // A descriptive error will be returned in the case of |
| paddy@57 | 170 // an invalid change. |
| paddy@38 | 171 func (c ProfileChange) Validate() error { |
| paddy@149 | 172 if c.Empty() { |
| paddy@48 | 173 return ErrEmptyChange |
| paddy@48 | 174 } |
| paddy@48 | 175 if c.PassphraseScheme != nil && c.Passphrase == nil { |
| paddy@48 | 176 return ErrMissingPassphrase |
| paddy@48 | 177 } |
| paddy@48 | 178 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil { |
| paddy@48 | 179 return ErrMissingPassphraseResetCreated |
| paddy@48 | 180 } |
| paddy@48 | 181 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil { |
| paddy@48 | 182 return ErrMissingPassphraseReset |
| paddy@48 | 183 } |
| paddy@48 | 184 if c.Salt != nil && c.Passphrase == nil { |
| paddy@48 | 185 return ErrMissingPassphrase |
| paddy@48 | 186 } |
| paddy@48 | 187 if c.Iterations != nil && c.Passphrase == nil { |
| paddy@48 | 188 return ErrMissingPassphrase |
| paddy@48 | 189 } |
| paddy@38 | 190 return nil |
| paddy@27 | 191 } |
| paddy@27 | 192 |
| paddy@57 | 193 // BulkProfileChange represents a single atomic change to many Profiles' mutable data. |
| paddy@57 | 194 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the |
| paddy@57 | 195 // ProfileChange values across many Profiles all at once. |
| paddy@44 | 196 type BulkProfileChange struct { |
| paddy@44 | 197 Compromised *bool |
| paddy@44 | 198 } |
| paddy@44 | 199 |
| paddy@149 | 200 func (b BulkProfileChange) Empty() bool { |
| paddy@149 | 201 return b.Compromised == nil |
| paddy@149 | 202 } |
| paddy@149 | 203 |
| paddy@57 | 204 // Validate checks the BulkProfileChange it is called on |
| paddy@57 | 205 // and asserts its internal validity, or lack thereof. |
| paddy@57 | 206 // A descriptive error will be returned in the case of an |
| paddy@57 | 207 // invalid change. |
| paddy@44 | 208 func (b BulkProfileChange) Validate() error { |
| paddy@149 | 209 if b.Empty() { |
| paddy@48 | 210 return ErrEmptyChange |
| paddy@48 | 211 } |
| paddy@44 | 212 return nil |
| paddy@44 | 213 } |
| paddy@44 | 214 |
| paddy@57 | 215 // Login represents a single human-friendly identifier for |
| paddy@57 | 216 // a given Profile that can be used to log into that Profile. |
| paddy@57 | 217 // Each Profile may only have one Login for each Type. |
| paddy@27 | 218 type Login struct { |
| paddy@172 | 219 Type string `json:"type,omitempty"` |
| paddy@172 | 220 Value string `json:"value,omitempty"` |
| paddy@172 | 221 ProfileID uuid.ID `json:"profile_id,omitempty"` |
| paddy@172 | 222 Created time.Time `json:"created,omitempty"` |
| paddy@172 | 223 LastUsed time.Time `json:"last_used,omitempty"` |
| paddy@178 | 224 Verification string `json:"verification,omitempty"` |
| paddy@172 | 225 Verified bool `json:"verified"` |
| paddy@172 | 226 } |
| paddy@172 | 227 |
| paddy@178 | 228 func (l Login) GetModelName() string { |
| paddy@178 | 229 return "logins" |
| paddy@178 | 230 } |
| paddy@178 | 231 |
| paddy@178 | 232 func (l Login) GetID() string { |
| paddy@178 | 233 return l.Value |
| paddy@178 | 234 } |
| paddy@178 | 235 |
| paddy@178 | 236 func (l Login) GetSystem() string { |
| paddy@178 | 237 return "code.secondbit.org/auth" |
| paddy@178 | 238 } |
| paddy@178 | 239 |
| paddy@172 | 240 type LoginChange struct { |
| paddy@172 | 241 Verification *string `json:"verification,omitempty"` |
| paddy@172 | 242 ResendVerification *bool `json:"resend_verification,omitempty"` |
| paddy@27 | 243 } |
| paddy@27 | 244 |
| paddy@99 | 245 type newProfileRequest struct { |
| paddy@99 | 246 Email string `json:"email"` |
| paddy@99 | 247 Passphrase string `json:"passphrase"` |
| paddy@99 | 248 Name string `json:"name"` |
| paddy@99 | 249 } |
| paddy@99 | 250 |
| paddy@172 | 251 func validateNewProfileRequest(req *newProfileRequest) []RequestError { |
| paddy@172 | 252 errors := []RequestError{} |
| paddy@99 | 253 req.Name = strings.TrimSpace(req.Name) |
| paddy@99 | 254 req.Email = strings.TrimSpace(req.Email) |
| paddy@99 | 255 if len(req.Passphrase) < MinPassphraseLength { |
| paddy@172 | 256 errors = append(errors, RequestError{ |
| paddy@172 | 257 Slug: RequestErrInsufficient, |
| paddy@99 | 258 Field: "/passphrase", |
| paddy@99 | 259 }) |
| paddy@99 | 260 } |
| paddy@99 | 261 if len(req.Passphrase) > MaxPassphraseLength { |
| paddy@172 | 262 errors = append(errors, RequestError{ |
| paddy@172 | 263 Slug: RequestErrOverflow, |
| paddy@99 | 264 Field: "/passphrase", |
| paddy@99 | 265 }) |
| paddy@99 | 266 } |
| paddy@99 | 267 if len(req.Name) > MaxNameLength { |
| paddy@172 | 268 errors = append(errors, RequestError{ |
| paddy@172 | 269 Slug: RequestErrOverflow, |
| paddy@99 | 270 Field: "/name", |
| paddy@99 | 271 }) |
| paddy@99 | 272 } |
| paddy@99 | 273 if req.Email == "" { |
| paddy@172 | 274 errors = append(errors, RequestError{ |
| paddy@172 | 275 Slug: RequestErrMissing, |
| paddy@99 | 276 Field: "/email", |
| paddy@99 | 277 }) |
| paddy@99 | 278 } |
| paddy@99 | 279 if len(req.Email) > MaxEmailLength { |
| paddy@172 | 280 errors = append(errors, RequestError{ |
| paddy@172 | 281 Slug: RequestErrOverflow, |
| paddy@99 | 282 Field: "/email", |
| paddy@99 | 283 }) |
| paddy@99 | 284 } |
| paddy@99 | 285 re := regexp.MustCompile(".+@.+\\..+") |
| paddy@99 | 286 if !re.Match([]byte(req.Email)) { |
| paddy@172 | 287 errors = append(errors, RequestError{ |
| paddy@172 | 288 Slug: RequestErrInvalidFormat, |
| paddy@99 | 289 Field: "/email", |
| paddy@99 | 290 }) |
| paddy@99 | 291 } |
| paddy@99 | 292 return errors |
| paddy@99 | 293 } |
| paddy@99 | 294 |
| paddy@57 | 295 type profileStore interface { |
| paddy@57 | 296 getProfileByID(id uuid.ID) (Profile, error) |
| paddy@69 | 297 getProfileByLogin(value string) (Profile, error) |
| paddy@57 | 298 saveProfile(profile Profile) error |
| paddy@57 | 299 updateProfile(id uuid.ID, change ProfileChange) error |
| paddy@57 | 300 updateProfiles(ids []uuid.ID, change BulkProfileChange) error |
| paddy@161 | 301 deleteProfile(id uuid.ID) error |
| paddy@44 | 302 |
| paddy@57 | 303 addLogin(login Login) error |
| paddy@172 | 304 getLogin(value string) (Login, error) |
| paddy@69 | 305 removeLogin(value string, profile uuid.ID) error |
| paddy@160 | 306 removeLoginsByProfile(profile uuid.ID) error |
| paddy@69 | 307 recordLoginUse(value string, when time.Time) error |
| paddy@172 | 308 verifyLogin(value, verification string) error |
| paddy@57 | 309 listLogins(profile uuid.ID, num, offset int) ([]Login, error) |
| paddy@38 | 310 } |
| paddy@27 | 311 |
| paddy@57 | 312 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) { |
| paddy@38 | 313 m.profileLock.RLock() |
| paddy@38 | 314 defer m.profileLock.RUnlock() |
| paddy@38 | 315 p, ok := m.profiles[id.String()] |
| paddy@38 | 316 if !ok { |
| paddy@38 | 317 return Profile{}, ErrProfileNotFound |
| paddy@38 | 318 } |
| paddy@38 | 319 return p, nil |
| paddy@27 | 320 } |
| paddy@38 | 321 |
| paddy@69 | 322 func (m *memstore) getProfileByLogin(value string) (Profile, error) { |
| paddy@44 | 323 m.loginLock.RLock() |
| paddy@44 | 324 defer m.loginLock.RUnlock() |
| paddy@69 | 325 login, ok := m.logins[value] |
| paddy@44 | 326 if !ok { |
| paddy@44 | 327 return Profile{}, ErrLoginNotFound |
| paddy@44 | 328 } |
| paddy@44 | 329 m.profileLock.RLock() |
| paddy@44 | 330 defer m.profileLock.RUnlock() |
| paddy@44 | 331 profile, ok := m.profiles[login.ProfileID.String()] |
| paddy@44 | 332 if !ok { |
| paddy@44 | 333 return Profile{}, ErrProfileNotFound |
| paddy@44 | 334 } |
| paddy@44 | 335 return profile, nil |
| paddy@38 | 336 } |
| paddy@38 | 337 |
| paddy@57 | 338 func (m *memstore) saveProfile(profile Profile) error { |
| paddy@38 | 339 m.profileLock.Lock() |
| paddy@38 | 340 defer m.profileLock.Unlock() |
| paddy@38 | 341 _, ok := m.profiles[profile.ID.String()] |
| paddy@38 | 342 if ok { |
| paddy@38 | 343 return ErrProfileAlreadyExists |
| paddy@38 | 344 } |
| paddy@38 | 345 m.profiles[profile.ID.String()] = profile |
| paddy@38 | 346 return nil |
| paddy@38 | 347 } |
| paddy@38 | 348 |
| paddy@57 | 349 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error { |
| paddy@38 | 350 m.profileLock.Lock() |
| paddy@38 | 351 defer m.profileLock.Unlock() |
| paddy@38 | 352 p, ok := m.profiles[id.String()] |
| paddy@38 | 353 if !ok { |
| paddy@38 | 354 return ErrProfileNotFound |
| paddy@38 | 355 } |
| paddy@38 | 356 p.ApplyChange(change) |
| paddy@38 | 357 m.profiles[id.String()] = p |
| paddy@38 | 358 return nil |
| paddy@38 | 359 } |
| paddy@38 | 360 |
| paddy@57 | 361 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error { |
| paddy@44 | 362 m.profileLock.Lock() |
| paddy@44 | 363 defer m.profileLock.Unlock() |
| paddy@44 | 364 for id, profile := range m.profiles { |
| paddy@44 | 365 for _, i := range ids { |
| paddy@44 | 366 if id == i.String() { |
| paddy@44 | 367 profile.ApplyBulkChange(change) |
| paddy@44 | 368 m.profiles[id] = profile |
| paddy@44 | 369 break |
| paddy@44 | 370 } |
| paddy@44 | 371 } |
| paddy@44 | 372 } |
| paddy@44 | 373 return nil |
| paddy@44 | 374 } |
| paddy@44 | 375 |
| paddy@161 | 376 func (m *memstore) deleteProfile(id uuid.ID) error { |
| paddy@161 | 377 m.profileLock.Lock() |
| paddy@161 | 378 defer m.profileLock.Unlock() |
| paddy@161 | 379 if _, ok := m.profiles[id.String()]; !ok { |
| paddy@161 | 380 return ErrProfileNotFound |
| paddy@161 | 381 } |
| paddy@161 | 382 delete(m.profiles, id.String()) |
| paddy@161 | 383 return nil |
| paddy@161 | 384 } |
| paddy@161 | 385 |
| paddy@57 | 386 func (m *memstore) addLogin(login Login) error { |
| paddy@44 | 387 m.loginLock.Lock() |
| paddy@44 | 388 defer m.loginLock.Unlock() |
| paddy@69 | 389 _, ok := m.logins[login.Value] |
| paddy@44 | 390 if ok { |
| paddy@44 | 391 return ErrLoginAlreadyExists |
| paddy@44 | 392 } |
| paddy@69 | 393 m.logins[login.Value] = login |
| paddy@69 | 394 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value) |
| paddy@44 | 395 return nil |
| paddy@44 | 396 } |
| paddy@44 | 397 |
| paddy@172 | 398 func (m *memstore) getLogin(value string) (Login, error) { |
| paddy@172 | 399 m.loginLock.RLock() |
| paddy@172 | 400 defer m.loginLock.RUnlock() |
| paddy@172 | 401 l, ok := m.logins[value] |
| paddy@172 | 402 if !ok { |
| paddy@172 | 403 return Login{}, ErrLoginNotFound |
| paddy@172 | 404 } |
| paddy@172 | 405 return l, nil |
| paddy@172 | 406 } |
| paddy@172 | 407 |
| paddy@69 | 408 func (m *memstore) removeLogin(value string, profile uuid.ID) error { |
| paddy@44 | 409 m.loginLock.Lock() |
| paddy@44 | 410 defer m.loginLock.Unlock() |
| paddy@69 | 411 l, ok := m.logins[value] |
| paddy@44 | 412 if !ok { |
| paddy@44 | 413 return ErrLoginNotFound |
| paddy@44 | 414 } |
| paddy@44 | 415 if !l.ProfileID.Equal(profile) { |
| paddy@44 | 416 return ErrLoginNotFound |
| paddy@44 | 417 } |
| paddy@69 | 418 delete(m.logins, value) |
| paddy@44 | 419 pos := -1 |
| paddy@44 | 420 for p, id := range m.profileLoginLookup[profile.String()] { |
| paddy@69 | 421 if id == value { |
| paddy@44 | 422 pos = p |
| paddy@44 | 423 break |
| paddy@44 | 424 } |
| paddy@44 | 425 } |
| paddy@44 | 426 if pos >= 0 { |
| paddy@44 | 427 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...) |
| paddy@44 | 428 } |
| paddy@44 | 429 return nil |
| paddy@44 | 430 } |
| paddy@44 | 431 |
| paddy@160 | 432 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error { |
| paddy@160 | 433 m.loginLock.Lock() |
| paddy@160 | 434 defer m.loginLock.Unlock() |
| paddy@160 | 435 logins, ok := m.profileLoginLookup[profile.String()] |
| paddy@160 | 436 if !ok { |
| paddy@160 | 437 return ErrProfileNotFound |
| paddy@160 | 438 } |
| paddy@160 | 439 delete(m.profileLoginLookup, profile.String()) |
| paddy@160 | 440 for _, login := range logins { |
| paddy@160 | 441 delete(m.logins, login) |
| paddy@160 | 442 } |
| paddy@160 | 443 return nil |
| paddy@160 | 444 } |
| paddy@160 | 445 |
| paddy@69 | 446 func (m *memstore) recordLoginUse(value string, when time.Time) error { |
| paddy@44 | 447 m.loginLock.Lock() |
| paddy@44 | 448 defer m.loginLock.Unlock() |
| paddy@69 | 449 l, ok := m.logins[value] |
| paddy@44 | 450 if !ok { |
| paddy@44 | 451 return ErrLoginNotFound |
| paddy@44 | 452 } |
| paddy@44 | 453 l.LastUsed = when |
| paddy@69 | 454 m.logins[value] = l |
| paddy@44 | 455 return nil |
| paddy@44 | 456 } |
| paddy@44 | 457 |
| paddy@172 | 458 func (m *memstore) verifyLogin(value, verification string) error { |
| paddy@172 | 459 m.loginLock.Lock() |
| paddy@172 | 460 defer m.loginLock.Unlock() |
| paddy@172 | 461 l, ok := m.logins[value] |
| paddy@172 | 462 if !ok { |
| paddy@172 | 463 return ErrLoginNotFound |
| paddy@172 | 464 } |
| paddy@172 | 465 if l.Verification != verification { |
| paddy@172 | 466 return ErrLoginVerificationInvalid |
| paddy@172 | 467 } |
| paddy@172 | 468 l.Verified = true |
| paddy@172 | 469 m.logins[value] = l |
| paddy@172 | 470 return nil |
| paddy@172 | 471 } |
| paddy@172 | 472 |
| paddy@57 | 473 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) { |
| paddy@44 | 474 m.loginLock.RLock() |
| paddy@44 | 475 defer m.loginLock.RUnlock() |
| paddy@44 | 476 ids, ok := m.profileLoginLookup[profile.String()] |
| paddy@44 | 477 if !ok { |
| paddy@44 | 478 return []Login{}, nil |
| paddy@44 | 479 } |
| paddy@44 | 480 if len(ids) > num+offset { |
| paddy@44 | 481 ids = ids[offset : num+offset] |
| paddy@44 | 482 } else if len(ids) > offset { |
| paddy@44 | 483 ids = ids[offset:] |
| paddy@44 | 484 } else { |
| paddy@44 | 485 return []Login{}, nil |
| paddy@44 | 486 } |
| paddy@44 | 487 logins := []Login{} |
| paddy@44 | 488 for _, id := range ids { |
| paddy@44 | 489 login, ok := m.logins[id] |
| paddy@44 | 490 if !ok { |
| paddy@44 | 491 continue |
| paddy@44 | 492 } |
| paddy@44 | 493 logins = append(logins, login) |
| paddy@44 | 494 } |
| paddy@44 | 495 return logins, nil |
| paddy@44 | 496 } |
| paddy@99 | 497 |
| paddy@160 | 498 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) { |
| paddy@160 | 499 err := context.RemoveLoginsByProfile(profile) |
| paddy@160 | 500 if err != nil { |
| paddy@160 | 501 log.Printf("Error removing logins from profile %s: %+v\n", profile, err) |
| paddy@160 | 502 } |
| paddy@162 | 503 err = context.TerminateSessionsByProfile(profile) |
| paddy@162 | 504 if err != nil { |
| paddy@162 | 505 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err) |
| paddy@162 | 506 } |
| paddy@162 | 507 err = context.RevokeTokensByProfileID(profile) |
| paddy@162 | 508 if err != nil { |
| paddy@162 | 509 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err) |
| paddy@162 | 510 } |
| paddy@163 | 511 err = context.DeleteAuthorizationCodesByProfileID(profile) |
| paddy@163 | 512 if err != nil { |
| paddy@163 | 513 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err) |
| paddy@163 | 514 } |
| paddy@164 | 515 clients, err := context.ListClientsByOwner(profile, -1, 0) |
| paddy@164 | 516 if err != nil { |
| paddy@164 | 517 log.Printf("Error listing clients by profile %s: %+v\n", profile, err) |
| paddy@164 | 518 } |
| paddy@164 | 519 err = context.DeleteClientsByOwner(profile) |
| paddy@164 | 520 if err != nil { |
| paddy@164 | 521 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err) |
| paddy@164 | 522 } |
| paddy@164 | 523 for _, client := range clients { |
| paddy@164 | 524 cleanUpAfterClientDeletion(client.ID, context) |
| paddy@164 | 525 } |
| paddy@160 | 526 } |
| paddy@160 | 527 |
| paddy@105 | 528 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval. |
| paddy@105 | 529 func RegisterProfileHandlers(r *mux.Router, context Context) { |
| paddy@165 | 530 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS") |
| paddy@166 | 531 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS") |
| paddy@165 | 532 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS") |
| paddy@165 | 533 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS") |
| paddy@128 | 534 // BUG(paddy): We need to implement a handler that will add a login to a profile. |
| paddy@128 | 535 // 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 | 536 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile. |
| paddy@172 | 537 r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS") |
| paddy@172 | 538 r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS") |
| paddy@105 | 539 } |
| paddy@105 | 540 |
| paddy@166 | 541 // GetProfileHandler is an HTTP handler for retrieving a profile. |
| paddy@166 | 542 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@172 | 543 errors := []RequestError{} |
| paddy@166 | 544 authz := r.Header.Get("Authorization") |
| paddy@166 | 545 if !strings.HasPrefix(authz, "Bearer ") { |
| paddy@172 | 546 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 547 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@166 | 548 return |
| paddy@166 | 549 } |
| paddy@166 | 550 authz = strings.TrimPrefix(authz, "Bearer ") |
| paddy@166 | 551 vars := mux.Vars(r) |
| paddy@166 | 552 if vars["id"] == "" { |
| paddy@172 | 553 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) |
| paddy@172 | 554 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@166 | 555 return |
| paddy@166 | 556 } |
| paddy@166 | 557 id, err := uuid.Parse(vars["id"]) |
| paddy@166 | 558 if err != nil { |
| paddy@172 | 559 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"}) |
| paddy@172 | 560 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@166 | 561 return |
| paddy@166 | 562 } |
| paddy@166 | 563 token, err := context.GetToken(authz, false) |
| paddy@166 | 564 if err != nil || token.Revoked { |
| paddy@166 | 565 if err == ErrTokenNotFound || token.Revoked { |
| paddy@172 | 566 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 567 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@166 | 568 return |
| paddy@166 | 569 } else { |
| paddy@166 | 570 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@166 | 571 return |
| paddy@166 | 572 } |
| paddy@166 | 573 } |
| paddy@166 | 574 if !id.Equal(token.ProfileID) { |
| paddy@172 | 575 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 576 encode(w, r, http.StatusForbidden, Response{Errors: errors}) |
| paddy@166 | 577 return |
| paddy@166 | 578 } |
| paddy@166 | 579 profile, err := context.GetProfileByID(id) |
| paddy@166 | 580 if err != nil { |
| paddy@166 | 581 if err == ErrProfileNotFound { |
| paddy@172 | 582 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"}) |
| paddy@172 | 583 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@166 | 584 return |
| paddy@166 | 585 } |
| paddy@166 | 586 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@166 | 587 return |
| paddy@166 | 588 } |
| paddy@172 | 589 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}}) |
| paddy@166 | 590 return |
| paddy@166 | 591 } |
| paddy@166 | 592 |
| paddy@99 | 593 // CreateProfileHandler is an HTTP handler for registering new profiles. |
| paddy@99 | 594 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@99 | 595 scheme, ok := passphraseSchemes[CurPassphraseScheme] |
| paddy@99 | 596 if !ok { |
| paddy@149 | 597 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme) |
| paddy@105 | 598 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 599 return |
| paddy@99 | 600 } |
| paddy@99 | 601 var req newProfileRequest |
| paddy@172 | 602 errors := []RequestError{} |
| paddy@99 | 603 decoder := json.NewDecoder(r.Body) |
| paddy@99 | 604 err := decoder.Decode(&req) |
| paddy@99 | 605 if err != nil { |
| paddy@105 | 606 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@99 | 607 return |
| paddy@99 | 608 } |
| paddy@99 | 609 errors = append(errors, validateNewProfileRequest(&req)...) |
| paddy@99 | 610 if len(errors) > 0 { |
| paddy@172 | 611 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@99 | 612 return |
| paddy@99 | 613 } |
| paddy@99 | 614 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations) |
| paddy@99 | 615 if err != nil { |
| paddy@149 | 616 log.Printf("Error creating encoded passphrase: %#+v\n", err) |
| paddy@105 | 617 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 618 return |
| paddy@99 | 619 } |
| paddy@99 | 620 profile := Profile{ |
| paddy@99 | 621 ID: uuid.NewID(), |
| paddy@99 | 622 Name: req.Name, |
| paddy@99 | 623 Passphrase: string(passphrase), |
| paddy@99 | 624 Iterations: context.config.iterations, |
| paddy@99 | 625 Salt: string(salt), |
| paddy@99 | 626 PassphraseScheme: CurPassphraseScheme, |
| paddy@99 | 627 Created: time.Now(), |
| paddy@99 | 628 LastSeen: time.Now(), |
| paddy@99 | 629 } |
| paddy@99 | 630 err = context.SaveProfile(profile) |
| paddy@99 | 631 if err != nil { |
| paddy@105 | 632 if err == ErrProfileAlreadyExists { |
| paddy@172 | 633 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}}) |
| paddy@105 | 634 return |
| paddy@105 | 635 } |
| paddy@149 | 636 log.Printf("Error saving profile: %#+v\n", err) |
| paddy@105 | 637 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 638 return |
| paddy@99 | 639 } |
| paddy@99 | 640 logins := []Login{} |
| paddy@99 | 641 login := Login{ |
| paddy@172 | 642 Type: "email", |
| paddy@172 | 643 Value: req.Email, |
| paddy@172 | 644 Created: profile.Created, |
| paddy@172 | 645 LastUsed: profile.Created, |
| paddy@172 | 646 ProfileID: profile.ID, |
| paddy@172 | 647 Verification: uuid.NewID().String(), |
| paddy@99 | 648 } |
| paddy@99 | 649 err = context.AddLogin(login) |
| paddy@99 | 650 if err != nil { |
| paddy@105 | 651 if err == ErrLoginAlreadyExists { |
| paddy@172 | 652 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}}) |
| paddy@105 | 653 return |
| paddy@105 | 654 } |
| paddy@149 | 655 log.Printf("Error adding login: %#+v\n", err) |
| paddy@105 | 656 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@99 | 657 return |
| paddy@99 | 658 } |
| paddy@178 | 659 verification := login.Verification |
| paddy@178 | 660 login.Verification = "" // clear verification so it's not exposed |
| paddy@99 | 661 logins = append(logins, login) |
| paddy@172 | 662 resp := Response{ |
| paddy@105 | 663 Logins: logins, |
| paddy@105 | 664 Profiles: []Profile{profile}, |
| paddy@105 | 665 } |
| paddy@105 | 666 encode(w, r, http.StatusCreated, resp) |
| paddy@178 | 667 login.Verification = verification // restore verification so it's included in the event |
| paddy@178 | 668 go context.SendModelEvent(login, events.ActionCreated) |
| paddy@178 | 669 go context.SendModelEvent(profile, events.ActionCreated) |
| paddy@99 | 670 } |
| paddy@145 | 671 |
| paddy@145 | 672 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@172 | 673 errors := []RequestError{} |
| paddy@145 | 674 vars := mux.Vars(r) |
| paddy@145 | 675 if vars["id"] == "" { |
| paddy@172 | 676 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) |
| paddy@172 | 677 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@145 | 678 return |
| paddy@145 | 679 } |
| paddy@145 | 680 id, err := uuid.Parse(vars["id"]) |
| paddy@145 | 681 if err != nil { |
| paddy@172 | 682 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 683 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@145 | 684 return |
| paddy@145 | 685 } |
| paddy@145 | 686 username, password, ok := r.BasicAuth() |
| paddy@145 | 687 if !ok { |
| paddy@172 | 688 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 689 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@145 | 690 return |
| paddy@145 | 691 } |
| paddy@145 | 692 profile, err := authenticate(username, password, context) |
| paddy@145 | 693 if err != nil { |
| paddy@145 | 694 if isAuthError(err) { |
| paddy@172 | 695 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 696 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@145 | 697 } else { |
| paddy@172 | 698 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 699 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@145 | 700 } |
| paddy@145 | 701 return |
| paddy@145 | 702 } |
| paddy@145 | 703 if !profile.ID.Equal(id) { |
| paddy@172 | 704 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 705 encode(w, r, http.StatusForbidden, Response{Errors: errors}) |
| paddy@145 | 706 return |
| paddy@145 | 707 } |
| paddy@145 | 708 var req ProfileChange |
| paddy@145 | 709 decoder := json.NewDecoder(r.Body) |
| paddy@145 | 710 err = decoder.Decode(&req) |
| paddy@145 | 711 if err != nil { |
| paddy@149 | 712 log.Printf("Error decoding request: %#+v\n", err) |
| paddy@145 | 713 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@145 | 714 return |
| paddy@145 | 715 } |
| paddy@145 | 716 req.Iterations = nil |
| paddy@145 | 717 req.Salt = nil |
| paddy@145 | 718 req.PassphraseScheme = nil |
| paddy@145 | 719 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised |
| paddy@145 | 720 req.LockedUntil = nil |
| paddy@145 | 721 req.LastSeen = nil |
| paddy@145 | 722 if req.Passphrase != nil { |
| paddy@145 | 723 if len(*req.Passphrase) < MinPassphraseLength { |
| paddy@172 | 724 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"}) |
| paddy@172 | 725 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@145 | 726 return |
| paddy@145 | 727 } |
| paddy@145 | 728 if len(*req.Passphrase) > MaxPassphraseLength { |
| paddy@172 | 729 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"}) |
| paddy@172 | 730 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@145 | 731 return |
| paddy@145 | 732 } |
| paddy@145 | 733 iterations := context.config.iterations |
| paddy@145 | 734 scheme, ok := passphraseSchemes[CurPassphraseScheme] |
| paddy@145 | 735 if !ok { |
| paddy@145 | 736 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@145 | 737 return |
| paddy@145 | 738 } |
| paddy@145 | 739 curScheme := CurPassphraseScheme |
| paddy@145 | 740 req.PassphraseScheme = &curScheme |
| paddy@145 | 741 passphrase, salt, err := scheme.create(*req.Passphrase, iterations) |
| paddy@145 | 742 if err != nil { |
| paddy@145 | 743 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@145 | 744 return |
| paddy@145 | 745 } |
| paddy@145 | 746 req.Passphrase = &passphrase |
| paddy@145 | 747 req.Salt = &salt |
| paddy@145 | 748 req.Iterations = &iterations |
| paddy@145 | 749 } |
| paddy@145 | 750 if req.PassphraseReset != nil { |
| paddy@145 | 751 now := time.Now() |
| paddy@145 | 752 req.PassphraseResetCreated = &now |
| paddy@145 | 753 } |
| paddy@145 | 754 err = req.Validate() |
| paddy@145 | 755 if err != nil { |
| paddy@145 | 756 var status int |
| paddy@172 | 757 var resp Response |
| paddy@145 | 758 switch err { |
| paddy@145 | 759 case ErrEmptyChange: |
| paddy@145 | 760 resp.Profiles = []Profile{profile} |
| paddy@145 | 761 status = http.StatusOK |
| paddy@145 | 762 default: |
| paddy@172 | 763 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@145 | 764 resp.Errors = errors |
| paddy@145 | 765 status = http.StatusInternalServerError |
| paddy@145 | 766 } |
| paddy@145 | 767 encode(w, r, status, resp) |
| paddy@145 | 768 return |
| paddy@145 | 769 } |
| paddy@145 | 770 err = context.UpdateProfile(id, req) |
| paddy@145 | 771 if err != nil { |
| paddy@145 | 772 if err == ErrProfileNotFound { |
| paddy@172 | 773 errors = append(errors, RequestError{Slug: RequestErrNotFound}) |
| paddy@172 | 774 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@145 | 775 return |
| paddy@145 | 776 } |
| paddy@145 | 777 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@145 | 778 return |
| paddy@145 | 779 } |
| paddy@145 | 780 profile.ApplyChange(req) |
| paddy@172 | 781 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}}) |
| paddy@145 | 782 return |
| paddy@145 | 783 } |
| paddy@160 | 784 |
| paddy@160 | 785 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@172 | 786 errors := []RequestError{} |
| paddy@160 | 787 vars := mux.Vars(r) |
| paddy@160 | 788 if vars["id"] == "" { |
| paddy@172 | 789 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"}) |
| paddy@172 | 790 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@160 | 791 return |
| paddy@160 | 792 } |
| paddy@160 | 793 id, err := uuid.Parse(vars["id"]) |
| paddy@160 | 794 if err != nil { |
| paddy@172 | 795 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 796 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@160 | 797 return |
| paddy@160 | 798 } |
| paddy@160 | 799 username, password, ok := r.BasicAuth() |
| paddy@160 | 800 if !ok { |
| paddy@172 | 801 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 802 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@160 | 803 return |
| paddy@160 | 804 } |
| paddy@160 | 805 profile, err := authenticate(username, password, context) |
| paddy@160 | 806 if err != nil { |
| paddy@160 | 807 if isAuthError(err) { |
| paddy@172 | 808 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 809 encode(w, r, http.StatusUnauthorized, Response{Errors: errors}) |
| paddy@160 | 810 } else { |
| paddy@172 | 811 errors = append(errors, RequestError{Slug: RequestErrActOfGod}) |
| paddy@172 | 812 encode(w, r, http.StatusInternalServerError, Response{Errors: errors}) |
| paddy@160 | 813 } |
| paddy@160 | 814 return |
| paddy@160 | 815 } |
| paddy@160 | 816 if !profile.ID.Equal(id) { |
| paddy@172 | 817 errors = append(errors, RequestError{Slug: RequestErrAccessDenied}) |
| paddy@172 | 818 encode(w, r, http.StatusForbidden, Response{Errors: errors}) |
| paddy@160 | 819 return |
| paddy@160 | 820 } |
| paddy@161 | 821 err = context.DeleteProfile(id) |
| paddy@160 | 822 if err != nil { |
| paddy@160 | 823 if err == ErrProfileNotFound { |
| paddy@172 | 824 errors = append(errors, RequestError{Slug: RequestErrNotFound}) |
| paddy@172 | 825 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@160 | 826 return |
| paddy@160 | 827 } |
| paddy@160 | 828 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@160 | 829 return |
| paddy@160 | 830 } |
| paddy@172 | 831 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}}) |
| paddy@160 | 832 go cleanUpAfterProfileDeletion(profile.ID, context) |
| paddy@160 | 833 } |
| paddy@172 | 834 |
| paddy@172 | 835 func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@172 | 836 var errors []RequestError |
| paddy@172 | 837 vars := mux.Vars(r) |
| paddy@172 | 838 if vars["login"] == "" { |
| paddy@172 | 839 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"}) |
| paddy@172 | 840 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@172 | 841 return |
| paddy@172 | 842 } |
| paddy@172 | 843 login, err := context.GetLogin(vars["login"]) |
| paddy@172 | 844 if err != nil { |
| paddy@172 | 845 if err == ErrLoginNotFound { |
| paddy@172 | 846 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"}) |
| paddy@172 | 847 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@172 | 848 return |
| paddy@172 | 849 } |
| paddy@172 | 850 log.Printf("Error retrieving login: %#+v\n", err) |
| paddy@172 | 851 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@172 | 852 return |
| paddy@172 | 853 } |
| paddy@178 | 854 // clear verification code so it's not exposed |
| paddy@178 | 855 // BUG(paddy): We hsould only hide the verification code if it's not an admin request, but auth isn't set up properly for scopes yet |
| paddy@178 | 856 login.Verification = "" |
| paddy@172 | 857 encode(w, r, http.StatusOK, Response{Logins: []Login{login}}) |
| paddy@172 | 858 } |
| paddy@172 | 859 |
| paddy@172 | 860 func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@172 | 861 var errors []RequestError |
| paddy@172 | 862 vars := mux.Vars(r) |
| paddy@172 | 863 if vars["login"] == "" { |
| paddy@172 | 864 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"}) |
| paddy@172 | 865 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@172 | 866 return |
| paddy@172 | 867 } |
| paddy@172 | 868 var req LoginChange |
| paddy@172 | 869 decoder := json.NewDecoder(r.Body) |
| paddy@172 | 870 err := decoder.Decode(&req) |
| paddy@172 | 871 if err != nil { |
| paddy@172 | 872 log.Printf("Error decoding request: %#+v\n", err) |
| paddy@172 | 873 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@172 | 874 return |
| paddy@172 | 875 } |
| paddy@172 | 876 login, err := context.GetLogin(vars["login"]) |
| paddy@172 | 877 if err != nil { |
| paddy@172 | 878 if err == ErrLoginNotFound { |
| paddy@172 | 879 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"}) |
| paddy@172 | 880 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@172 | 881 return |
| paddy@172 | 882 } |
| paddy@172 | 883 log.Printf("Error retrieving login: %#+v\n", err) |
| paddy@172 | 884 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@172 | 885 return |
| paddy@172 | 886 } |
| paddy@172 | 887 if req.Verification != nil { |
| paddy@172 | 888 err = context.VerifyLogin(vars["login"], *req.Verification) |
| paddy@172 | 889 if err != nil { |
| paddy@172 | 890 if err == ErrLoginNotFound { |
| paddy@172 | 891 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"}) |
| paddy@172 | 892 encode(w, r, http.StatusNotFound, Response{Errors: errors}) |
| paddy@172 | 893 return |
| paddy@172 | 894 } else if err == ErrLoginVerificationInvalid { |
| paddy@172 | 895 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"}) |
| paddy@172 | 896 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@172 | 897 return |
| paddy@172 | 898 } |
| paddy@172 | 899 log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err) |
| paddy@172 | 900 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@172 | 901 return |
| paddy@172 | 902 } |
| paddy@179 | 903 go context.SendModelEvent(login, ActionLoginVerified) |
| paddy@172 | 904 login.Verified = true |
| paddy@172 | 905 } else if req.ResendVerification != nil { |
| paddy@172 | 906 if !*req.ResendVerification { |
| paddy@172 | 907 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"}) |
| paddy@172 | 908 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@172 | 909 return |
| paddy@172 | 910 } |
| paddy@178 | 911 go context.SendModelEvent(login, ActionResendVerification) |
| paddy@172 | 912 } else { |
| paddy@172 | 913 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"}) |
| paddy@172 | 914 encode(w, r, http.StatusBadRequest, Response{Errors: errors}) |
| paddy@172 | 915 return |
| paddy@172 | 916 } |
| paddy@178 | 917 // clear the Verification code so it's not exposed |
| paddy@178 | 918 login.Verification = "" |
| paddy@172 | 919 encode(w, r, http.StatusOK, Response{Logins: []Login{login}}) |
| paddy@172 | 920 } |