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