auth
auth/profile.go
Update client to detect errors. The client doesn't treat non-200 responses as errors automatically, so we need to detect when the response.Errors property is set, and use that to return an error. To avoid the boilerplate and an extensive error system, I just wrapped them in an httpErrors type that implements the error interface. That way the errors can be returned, and callers can type-cast and interrogate them. I also updated the GetLogin function to return an auth.ErrLoginNotFound error when the httpErrors response indicates that's the reason the request failed.
| 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 } |