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