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