diff --git a/actions.go b/actions.go index 596350a2b82e6c0a9be8adef1cb525dfa61e2059..934d163647a5b54752f417e0cdc1516868e081c2 100644 --- a/actions.go +++ b/actions.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "encoding/base64" + "errors" "fmt" "git.autistici.org/ai3/go-common/pwhash" @@ -162,7 +163,7 @@ func (r *PrivilegedRequestBase) Authorize(rctx *RequestContext) error { } // TODO: call out to the auth-server if !pwhash.ComparePassword(rctx.User.Password, r.CurPassword) { - return fmt.Errorf("bad password (%s vs %s)", rctx.User.Password, r.CurPassword) + return errors.New("bad password") } return nil } diff --git a/actions_create.go b/actions_create.go index 8ca2d64bd8b9780b311d048276e2bce231feaefa..5898304880c35551bf9ac03ae01c4ed043d1457d 100644 --- a/actions_create.go +++ b/actions_create.go @@ -118,7 +118,7 @@ func (r *CreateUserRequest) applyTemplate(rctx *RequestContext) error { r.User.Has2FA = false r.User.HasOTP = false r.User.HasEncryptionKeys = true // set to true so that resetPassword will create keys. - r.User.PasswordRecoveryHint = "" + r.User.AccountRecoveryHint = "" r.User.AppSpecificPasswords = nil r.User.U2FRegistrations = nil if r.User.Lang == "" { diff --git a/actions_test.go b/actions_test.go index 964dbe77e942f9b03bd17f14946080c6354b0dd0..85b4b89aa2b081bbd7d3ab42fc017edb8a61ccf0 100644 --- a/actions_test.go +++ b/actions_test.go @@ -73,14 +73,14 @@ func (b *fakeBackend) SetUserPassword(_ context.Context, user *User, password st return nil } -func (b *fakeBackend) SetPasswordRecoveryHint(_ context.Context, user *User, hint, response string) error { - b.users[user.Name].PasswordRecoveryHint = hint +func (b *fakeBackend) SetAccountRecoveryHint(_ context.Context, user *User, hint, response string) error { + b.users[user.Name].AccountRecoveryHint = hint b.recoveryPasswords[user.Name] = response return nil } -func (b *fakeBackend) DeletePasswordRecoveryHint(_ context.Context, user *User) error { - b.users[user.Name].PasswordRecoveryHint = "" +func (b *fakeBackend) DeleteAccountRecoveryHint(_ context.Context, user *User) error { + b.users[user.Name].AccountRecoveryHint = "" delete(b.recoveryPasswords, user.Name) return nil } @@ -667,7 +667,7 @@ func TestService_ResetResourcePassword(t *testing.T) { // svc := testService("") // // Bad recovery response. -// _, err := svc.RecoverPassword(context.Background(), tx, &PasswordRecoveryRequest{ +// _, err := svc.RecoverPassword(context.Background(), tx, &AccountRecoveryRequest{ // Username: "testuser", // RecoveryPassword: "BADPASS", // Password: "new_password", @@ -677,7 +677,7 @@ func TestService_ResetResourcePassword(t *testing.T) { // } // // Successful account recovery. -// _, err = svc.RecoverPassword(context.Background(), tx, &PasswordRecoveryRequest{ +// _, err = svc.RecoverPassword(context.Background(), tx, &AccountRecoveryRequest{ // Username: "testuser", // RecoveryPassword: "recoverypassword", // Password: "new_password", diff --git a/actions_user.go b/actions_user.go index f1c16a459402456cbdadfb8463310c7402b1d4bc..5d9ee7e11ded547f21c043e64c8a1fa7b49fbbd7 100644 --- a/actions_user.go +++ b/actions_user.go @@ -49,25 +49,25 @@ func (r *ChangeUserPasswordRequest) Serve(rctx *RequestContext) (interface{}, er return nil, err } -// PasswordRecoveryRequest lets users reset their password by providing +// AccountRecoveryRequest lets users reset their password by providing // secondary credentials, which we authenticate ourselves. It is not // authenticated with SSO. // // Two-factor authentication is disabled on successful recovery. -type PasswordRecoveryRequest struct { +type AccountRecoveryRequest struct { Username string `json:"username"` RecoveryPassword string `json:"recovery_password"` Password string `json:"password"` // TODO: add DeviceInfo } -// PasswordRecoveryResponse is the response type for PasswordRecoveryRequest. -type PasswordRecoveryResponse struct { +// AccountRecoveryResponse is the response type for AccountRecoveryRequest. +type AccountRecoveryResponse struct { Hint string `json:"hint,optional"` } // Sanitize the request. -func (r *PasswordRecoveryRequest) Sanitize() { +func (r *AccountRecoveryRequest) Sanitize() { if r.RecoveryPassword != "" { r.RecoveryPassword = sanitizedValue } @@ -77,16 +77,19 @@ func (r *PasswordRecoveryRequest) Sanitize() { } // Validate the request. -func (r *PasswordRecoveryRequest) Validate(rctx *RequestContext) error { - if err := rctx.fieldValidators.password(rctx.Context, r.Password); err != nil { - return newValidationError(nil, "password", err.Error()) +func (r *AccountRecoveryRequest) Validate(rctx *RequestContext) error { + // Only validate the password if attempting recovery. + if r.RecoveryPassword != "" { + if err := rctx.fieldValidators.password(rctx.Context, r.Password); err != nil { + return newValidationError(nil, "password", err.Error()) + } } return nil } // PopulateContext extracts information from the request and stores it // into the RequestContext. -func (r *PasswordRecoveryRequest) PopulateContext(rctx *RequestContext) error { +func (r *AccountRecoveryRequest) PopulateContext(rctx *RequestContext) error { user, err := getUserOrDie(rctx.Context, rctx.TX, r.Username) if err != nil { return err @@ -96,7 +99,7 @@ func (r *PasswordRecoveryRequest) PopulateContext(rctx *RequestContext) error { } // Authorize the request. -func (r *PasswordRecoveryRequest) Authorize(rctx *RequestContext) error { +func (r *AccountRecoveryRequest) Authorize(rctx *RequestContext) error { // Anyone can request the hint (rate-limit above this layer). if r.RecoveryPassword == "" { return nil @@ -112,9 +115,9 @@ func (r *PasswordRecoveryRequest) Authorize(rctx *RequestContext) error { } // Serve the request. -func (r *PasswordRecoveryRequest) Serve(rctx *RequestContext) (interface{}, error) { - resp := PasswordRecoveryResponse{ - Hint: rctx.User.PasswordRecoveryHint, +func (r *AccountRecoveryRequest) Serve(rctx *RequestContext) (interface{}, error) { + resp := AccountRecoveryResponse{ + Hint: rctx.User.AccountRecoveryHint, } // Only attempt to authenticate if the recovery password is @@ -172,16 +175,16 @@ func (r *ResetPasswordRequest) Serve(rctx *RequestContext) (interface{}, error) return nil, nil } -// SetPasswordRecoveryHintRequest lets users set the password recovery hint +// SetAccountRecoveryHintRequest lets users set the password recovery hint // and expected response (secondary password). -type SetPasswordRecoveryHintRequest struct { +type SetAccountRecoveryHintRequest struct { PrivilegedRequestBase Hint string `json:"recovery_hint"` Response string `json:"recovery_response"` } // Validate the request. -func (r *SetPasswordRecoveryHintRequest) Validate(rctx *RequestContext) error { +func (r *SetAccountRecoveryHintRequest) Validate(rctx *RequestContext) error { var err *validationError if r.Hint == "" { err = newValidationError(err, "recovery_hint", "mandatory field") @@ -189,12 +192,12 @@ func (r *SetPasswordRecoveryHintRequest) Validate(rctx *RequestContext) error { if verr := rctx.fieldValidators.password(rctx.Context, r.Response); verr != nil { err = newValidationError(err, "recovery_response", verr.Error()) } - return err + return err.orNil() } // Serve the request. -func (r *SetPasswordRecoveryHintRequest) Serve(rctx *RequestContext) (interface{}, error) { - return nil, rctx.User.setPasswordRecoveryHint(rctx.Context, rctx.TX, r.CurPassword, r.Hint, r.Response) +func (r *SetAccountRecoveryHintRequest) Serve(rctx *RequestContext) (interface{}, error) { + return nil, rctx.User.setAccountRecoveryHint(rctx.Context, rctx.TX, r.CurPassword, r.Hint, r.Response) } // CreateApplicationSpecificPasswordRequest lets users create their own ASPs. diff --git a/backend/model.go b/backend/model.go index f3e9d73afc414811cb92cc928491146584aefa02..8add2f31817bed60c68f92b23deab8834643f1af 100644 --- a/backend/model.go +++ b/backend/model.go @@ -120,16 +120,17 @@ func newUser(entry *ldap.Entry) (*as.RawUser, error) { // // The case of password recovery attributes is more complex: // the current schema has those on email=, but we'd like to - // move them to uid=, so we currently have to support both. + // move them to uid=, so we currently have to support both + // (but the uid= one takes precedence). uidNumber, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint user := &as.RawUser{ User: as.User{ - Name: entry.GetAttributeValue("uid"), - Lang: entry.GetAttributeValue(preferredLanguageLDAPAttr), - UID: uidNumber, - PasswordRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr), - U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)), - HasOTP: entry.GetAttributeValue(totpSecretLDAPAttr) != "", + Name: entry.GetAttributeValue("uid"), + Lang: entry.GetAttributeValue(preferredLanguageLDAPAttr), + UID: uidNumber, + AccountRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr), + U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)), + HasOTP: entry.GetAttributeValue(totpSecretLDAPAttr) != "", }, // Remove the legacy LDAP {crypt} prefix on old passwords. Password: strings.TrimPrefix(entry.GetAttributeValue(passwordLDAPAttr), "{crypt}"), @@ -261,13 +262,19 @@ func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.RawUser, } for _, entry := range result.Entries { - // Some user-level attributes are actually stored on the email - // object, a shortcoming of the legacy A/I database model. Set - // them on the main User object. + // Some user-level attributes are actually stored on + // the email object, which is desired in some cases, + // but in others is a shortcoming of the legacy A/I + // database model. Set them on the main User + // object. For the latter, attributes on the main User + // object take precedence. if isObjectClass(entry, "virtualMailUser") { - if s := entry.GetAttributeValue(recoveryHintLDAPAttr); s != "" { - user.PasswordRecoveryHint = s + if user.AccountRecoveryHint == "" { + if s := entry.GetAttributeValue(recoveryHintLDAPAttr); s != "" { + user.AccountRecoveryHint = s + } } + user.AppSpecificPasswords = getASPInfo(decodeAppSpecificPasswords(entry.GetAttributeValues(aspLDAPAttr))) user.Keys = decodeUserEncryptionKeys( entry.GetAttributeValues(storagePrivateKeyLDAPAttr)) @@ -297,7 +304,7 @@ func (tx *backendTX) SetUserPassword(ctx context.Context, user *as.User, encrypt return } -func (tx *backendTX) SetPasswordRecoveryHint(ctx context.Context, user *as.User, hint, response string) error { +func (tx *backendTX) SetAccountRecoveryHint(ctx context.Context, user *as.User, hint, response string) error { // Write the password recovery attributes on the uid= object, // as per the new schema. dn := tx.getUserDN(user) @@ -306,7 +313,7 @@ func (tx *backendTX) SetPasswordRecoveryHint(ctx context.Context, user *as.User, return nil } -func (tx *backendTX) DeletePasswordRecoveryHint(ctx context.Context, user *as.User) error { +func (tx *backendTX) DeleteAccountRecoveryHint(ctx context.Context, user *as.User) error { // Write the password recovery attributes on the uid= object, // as per the new schema. dn := tx.getUserDN(user) diff --git a/errors.go b/errors.go index 7ff6ffbceb1c3bae9e059a844d190c8a66fbf60a..691a209d0b34f8332a5f04b83656ba1864b3913d 100644 --- a/errors.go +++ b/errors.go @@ -85,8 +85,12 @@ func newValidationError(err *validationError, field, msg string) *validationErro func (v *validationError) Error() string { var p []string - for f, l := range v.fields { - p = append(p, fmt.Sprintf("%s: %s", f, strings.Join(l, ", "))) + if v.fields != nil { + for f, l := range v.fields { + p = append(p, fmt.Sprintf("%s: %s", f, strings.Join(l, ", "))) + } + } else { + p = append(p, "INVALID ERROR") } return fmt.Sprintf("request validation error: %s", strings.Join(p, "; ")) } @@ -95,3 +99,10 @@ func (v *validationError) JSON() []byte { data, _ := json.Marshal(v.fields) // nolint return data } + +func (v *validationError) orNil() error { + if v == nil { + return nil + } + return v +} diff --git a/integrationtest/integration_test.go b/integrationtest/integration_test.go index 9884f59ce4bf92c6a3d5ecbe4281d793aa0c2c78..355d39d895ad671dda9bdd06e73bd9bf05ab7942 100644 --- a/integrationtest/integration_test.go +++ b/integrationtest/integration_test.go @@ -2,6 +2,7 @@ package integrationtest import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -17,6 +18,8 @@ import ( "git.autistici.org/ai3/accountserver/backend" "git.autistici.org/ai3/accountserver/ldaptest" "git.autistici.org/ai3/accountserver/server" + "git.autistici.org/ai3/go-common/pwhash" + "git.autistici.org/ai3/go-common/userenckey" "git.autistici.org/id/go-sso" "golang.org/x/crypto/ed25519" ) @@ -99,12 +102,16 @@ func (c *testClient) request(uri string, req, out interface{}) error { return json.Unmarshal(data, out) } -func startService(t testing.TB) (func(), *testClient) { +func startService(t testing.TB) (func(), as.Backend, *testClient) { stop := ldaptest.StartServer(t, &ldaptest.Config{ - Dir: "../ldaptest", - Port: testLDAPPort, - Base: "dc=example,dc=com", - LDIFs: []string{"testdata/base.ldif", "testdata/test1.ldif"}, + Dir: "../ldaptest", + Port: testLDAPPort, + Base: "dc=example,dc=com", + LDIFs: []string{ + "testdata/base.ldif", + "testdata/test1.ldif", + "testdata/test2.ldif", + }, }) be, err := backend.NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com") @@ -151,14 +158,14 @@ func startService(t testing.TB) (func(), *testClient) { stop() srv.Close() ssoStop() - }, c + }, be, c } // Verify that authentication on GetUser works as expected: // - users can fetch their own data but not other users' // - admins can read everything. func TestIntegration_GetUser_Auth(t *testing.T) { - stop, c := startService(t) + stop, _, c := startService(t) defer stop() testdata := []struct { @@ -194,16 +201,16 @@ func TestIntegration_GetUser_Auth(t *testing.T) { } } -// Verify that a user can change their password. -func TestIntegration_ChangeUserPassword(t *testing.T) { - stop, c := startService(t) +// Verify that a user can't change someone else's password. +func TestIntegration_ChangeUserPassword_AuthFail(t *testing.T) { + stop, _, c := startService(t) defer stop() err := c.request("/api/user/change_password", &as.ChangeUserPasswordRequest{ PrivilegedRequestBase: as.PrivilegedRequestBase{ UserRequestBase: as.UserRequestBase{ RequestBase: as.RequestBase{ - SSO: c.ssoTicket("uno@investici.org"), + SSO: c.ssoTicket("due@investici.org"), }, Username: "uno@investici.org", }, @@ -212,14 +219,23 @@ func TestIntegration_ChangeUserPassword(t *testing.T) { Password: "new_password", }, nil) - if err != nil { - t.Fatal("ChangePassword", err) + if err == nil { + t.Fatal("ChangePassword for another user succeeded") } } -// Verify that changing the password sets user encryption keys. -func TestIntegration_ChangeUserPassword_SetsEncryptionKeys(t *testing.T) { - stop, c := startService(t) +// Verify various attempts at changing the password (user has no encryption keys). +func TestIntegration_ChangeUserPassword(t *testing.T) { + runChangeUserPasswordTest(t, "uno@investici.org") +} + +// Verify various attempts at changing the password (user with encryption keys). +func TestIntegration_ChangeUserPassword_WithEncryptionKeys(t *testing.T) { + runChangeUserPasswordTest(t, "due@investici.org") +} + +func runChangeUserPasswordTest(t *testing.T, username string) { + stop, _, c := startService(t) defer stop() testdata := []struct { @@ -239,9 +255,9 @@ func TestIntegration_ChangeUserPassword_SetsEncryptionKeys(t *testing.T) { PrivilegedRequestBase: as.PrivilegedRequestBase{ UserRequestBase: as.UserRequestBase{ RequestBase: as.RequestBase{ - SSO: c.ssoTicket("uno@investici.org"), + SSO: c.ssoTicket(username), }, - Username: "uno@investici.org", + Username: username, }, CurPassword: td.password, }, @@ -256,7 +272,7 @@ func TestIntegration_ChangeUserPassword_SetsEncryptionKeys(t *testing.T) { } func TestIntegration_CreateResource(t *testing.T) { - stop, c := startService(t) + stop, _, c := startService(t) defer stop() testdata := []struct { @@ -376,7 +392,7 @@ func TestIntegration_CreateResource(t *testing.T) { } func TestIntegration_CreateMultipleResources_WithTemplate(t *testing.T) { - stop, c := startService(t) + stop, _, c := startService(t) defer stop() // The create request is very bare, most values will be filled @@ -407,3 +423,130 @@ func TestIntegration_CreateMultipleResources_WithTemplate(t *testing.T) { t.Errorf("CreateResources failed: %v", err) } } + +func TestIntegration_CreateUser(t *testing.T) { + stop, be, c := startService(t) + defer stop() + + // The create request is very bare, most values will be filled + // in by the server using resource templates. + var resp as.CreateUserResponse + err := c.request("/api/user/create", &as.CreateUserRequest{ + AdminRequestBase: as.AdminRequestBase{ + RequestBase: as.RequestBase{ + SSO: c.ssoTicket(testAdminUser), + }, + }, + User: &as.User{ + Name: "newuser@example.com", + Resources: []*as.Resource{ + &as.Resource{ + ID: as.NewResourceID(as.ResourceTypeEmail, "newuser@example.com", "newuser@example.com"), + Name: "newuser@example.com", + }, + }, + }, + }, &resp) + if err != nil { + t.Fatalf("CreateUser failed: %v", err) + } + + if resp.Password == "" { + t.Fatalf("no password in response (%v)", resp) + } + + // Verify that the new password works. + checkUserInvariants(t, be, "newuser@example.com", resp.Password) +} + +func TestIntegration_AccountRecovery(t *testing.T) { + runAccountRecoveryTest(t, "uno@investici.org") +} + +func TestIntegration_AccountRecovery_WithEncryptionKeys(t *testing.T) { + user := runAccountRecoveryTest(t, "due@investici.org") + if !user.HasEncryptionKeys { + t.Fatalf("encryption keys not enabled after account recovery") + } +} + +func runAccountRecoveryTest(t *testing.T, username string) *as.RawUser { + stop, be, c := startService(t) + defer stop() + + hint := "secret code?" + secondaryPw := "open sesame!" + err := c.request("/api/user/set_account_recovery_hint", &as.SetAccountRecoveryHintRequest{ + PrivilegedRequestBase: as.PrivilegedRequestBase{ + UserRequestBase: as.UserRequestBase{ + RequestBase: as.RequestBase{ + SSO: c.ssoTicket(username), + }, + Username: username, + }, + CurPassword: "password", + }, + Hint: hint, + Response: secondaryPw, + }, nil) + if err != nil { + t.Fatalf("SetAccountRecoveryHint failed: %v", err) + } + + // The first request just fetches the recovery hint. + var resp as.AccountRecoveryResponse + err = c.request("/api/recover_account", &as.AccountRecoveryRequest{ + Username: username, + }, &resp) + if err != nil { + t.Fatalf("AccountRecovery (hint only) failed: %v", err) + } + if resp.Hint != hint { + t.Fatalf("bad AccountRecovery hint, got '%s' expected '%s'", resp.Hint, hint) + } + + // Now recover the account and set a new password. + newPw := "new password" + err = c.request("/api/recover_account", &as.AccountRecoveryRequest{ + Username: username, + RecoveryPassword: secondaryPw, + Password: newPw, + }, &resp) + if err != nil { + t.Fatalf("AccountRecovery failed: %v", err) + } + + return checkUserInvariants(t, be, username, newPw) +} + +// Verify that some user authentication invariants are true. Returns +// the RawUser for further checks. +func checkUserInvariants(t *testing.T, be as.Backend, username, primaryPassword string) *as.RawUser { + tx, _ := be.NewTransaction() + user, err := tx.GetUser(context.Background(), username) + if err != nil { + t.Fatalf("GetUser(%s): %v", username, err) + } + + // Verify that the password is correct. + if !pwhash.ComparePassword(user.Password, primaryPassword) { + t.Fatalf("password for user %s is not %s", username, primaryPassword) + } + + // Verify that we can successfully encrypt keys. + if user.HasEncryptionKeys { + if _, err := userenckey.Decrypt(keysToBytes(user.Keys), []byte(primaryPassword)); err != nil { + t.Fatalf("password for user %s can't decrypt keys", username) + } + } + + return user +} + +func keysToBytes(keys []*as.UserEncryptionKey) [][]byte { + var rawKeys [][]byte + for _, k := range keys { + rawKeys = append(rawKeys, k.Key) + } + return rawKeys +} diff --git a/integrationtest/testdata/test2.ldif b/integrationtest/testdata/test2.ldif new file mode 100644 index 0000000000000000000000000000000000000000..989b55e75c39711162eafeda065147a039e91b89 --- /dev/null +++ b/integrationtest/testdata/test2.ldif @@ -0,0 +1,87 @@ +dn: uid=due@investici.org,ou=People,dc=example,dc=com +cn: due@investici.org +gecos: due@investici.org +gidNumber: 2000 +givenName: Private +homeDirectory: /var/empty +loginShell: /bin/false +objectClass: top +objectClass: person +objectClass: posixAccount +objectClass: shadowAccount +objectClass: organizationalPerson +objectClass: inetOrgPerson +shadowLastChange: 12345 +shadowMax: 99999 +shadowWarning: 7 +sn: due@investici.org +uid: due@investici.org +uidNumber: 256799 +userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0 + +dn: mail=due@investici.org,uid=due@investici.org,ou=People,dc=example,dc=com +creationDate: 01-08-2013 +gidNumber: 2000 +host: host2 +mail: due@investici.org +mailMessageStore: investici.org/due/ +objectClass: top +objectClass: virtualMailUser +originalHost: host2 +status: active +uidNumber: 256799 +userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0 +storagePublicKey:: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUU0d0VBWUhLb1pJemowQ0FRWUZLNEVFQUNFRE9nQUVKaDFSdW1MTkt3M1dBTXNSMFFpMVRrc2pJSC9udCtwaApGRjZjdmhPZFN3anhsOVhFVVovVzlONWdDcUlqbGl0eFk2VHdSZWlzZThrPQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0K +storageEncryptedSecretKey:: bWFpbjoBAQAAAAAQAAAEIKLsDnzqCxHAmVXiSi3WT77YqNDY5sW0les/rC0owl0MegC3frvzAGG7vr6PhYNozksn2Fw4tQbj7G52HeQr3V8R58J3F2CHLdiwLGDMKCNy1hjyCN6rXCp1OQqg4VWtEovumogA4FaJtZS74WnCP2YGcxJy/0Am3U2TlFmx0e0jzuCk9lZ8piX+YKR6c8Qh/bv5vjq2gZ9AO2nh5Q== + +dn: ftpname=due,uid=due@investici.org,ou=People,dc=example,dc=com +status: active +givenName: Private +cn: due +objectClass: top +objectClass: person +objectClass: posixAccount +objectClass: shadowAccount +objectClass: organizationalPerson +objectClass: inetOrgPerson +objectClass: ftpAccount +loginShell: /bin/false +userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0 +shadowWarning: 7 +uidNumber: 256799 +host: host2 +shadowMax: 99999 +ftpname: due +gidNumber: 33 +gecos: FTP Account for due@investici.org +sn: Private +homeDirectory: /home/users/investici.org/due +uid: due +creationDate: 01-08-2013 +shadowLastChange: 12345 +originalHost: host2 + +dn: alias=due,uid=due@investici.org,ou=People,dc=example,dc=com +status: active +parentSite: autistici.org +objectClass: top +objectClass: subSite +alias: due +host: host2 +documentRoot: /home/users/investici.org/due/html-due +creationDate: 01-08-2013 +originalHost: host2 +statsId: 2193 +option: php + +dn: dbname=due,alias=due,uid=due@investici.org,ou=People,dc=example,dc=com +clearPassword: tae1tei8eir7wae1OZaeXXX +dbname: due +dbuser: due +host: host2 +originalHost: host2 +mysqlPassword: *7DD66AA8CD1E7687B993732C3A87CFFA43B95E27 +objectClass: top +objectClass: dbMysql +status: active + diff --git a/server/server.go b/server/server.go index e36de4158220a1beb5ba0256b8aca2bbeb61211d..3cb32aaf7e6eee7ed86e14e9e6a7c854e8d473c2 100644 --- a/server/server.go +++ b/server/server.go @@ -34,15 +34,15 @@ func (r *actionRegistry) newRequest(path string) (as.Request, bool) { return reflect.New(h).Interface().(as.Request), true } -// AccountServer is the HTTP API interface to AccountService. -type AccountServer struct { +// APIServer is the HTTP API interface to AccountService. +type APIServer struct { *actionRegistry service *as.AccountService } -// New creates a new AccountServer. -func New(service *as.AccountService, backend as.Backend) *AccountServer { - s := &AccountServer{ +// New creates a new APIServer. +func New(service *as.AccountService, backend as.Backend) *APIServer { + s := &APIServer{ actionRegistry: newActionRegistry(), service: service, } @@ -51,7 +51,7 @@ func New(service *as.AccountService, backend as.Backend) *AccountServer { s.Register("/api/user/create", &as.CreateUserRequest{}) s.Register("/api/user/update", &as.UpdateUserRequest{}) s.Register("/api/user/change_password", &as.ChangeUserPasswordRequest{}) - s.Register("/api/user/set_password_recovery_hint", &as.SetPasswordRecoveryHintRequest{}) + s.Register("/api/user/set_account_recovery_hint", &as.SetAccountRecoveryHintRequest{}) s.Register("/api/user/enable_otp", &as.EnableOTPRequest{}) s.Register("/api/user/disable_otp", &as.DisableOTPRequest{}) s.Register("/api/user/create_app_specific_password", &as.CreateApplicationSpecificPasswordRequest{}) @@ -63,14 +63,14 @@ func New(service *as.AccountService, backend as.Backend) *AccountServer { s.Register("/api/resource/reset_password", &as.ResetPasswordRequest{}) s.Register("/api/resource/email/add_alias", &as.AddEmailAliasRequest{}) s.Register("/api/resource/email/delete_alias", &as.DeleteEmailAliasRequest{}) - s.Register("/api/recover_password", &as.PasswordRecoveryRequest{}) + s.Register("/api/recover_account", &as.AccountRecoveryRequest{}) return s } var emptyResponse struct{} -func (s *AccountServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { +func (s *APIServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Create a new empty request object based on the request // path, then decode the HTTP request JSON body onto it. r, ok := s.newRequest(req.URL.Path) diff --git a/service.go b/service.go index 0350dd8d4d3b380406167a6ae9c654fe1d759846..837f2bebf02d49c794fb377b74201b4215c20531 100644 --- a/service.go +++ b/service.go @@ -47,8 +47,8 @@ type TX interface { UpdateUser(context.Context, *User) error CreateUser(context.Context, *User) error SetUserPassword(context.Context, *User, string) error - SetPasswordRecoveryHint(context.Context, *User, string, string) error - DeletePasswordRecoveryHint(context.Context, *User) error + SetAccountRecoveryHint(context.Context, *User, string, string) error + DeleteAccountRecoveryHint(context.Context, *User) error SetUserEncryptionKeys(context.Context, *User, []*UserEncryptionKey) error SetUserEncryptionPublicKey(context.Context, *User, []byte) error SetApplicationSpecificPassword(context.Context, *User, *AppSpecificPasswordInfo, string) error diff --git a/types.go b/types.go index e6bac78331b1b7ac7a12092425f1299a507773de..5e5ed5fcef66abaf6d88c07a710fce19c1f968ea 100644 --- a/types.go +++ b/types.go @@ -39,7 +39,7 @@ type User struct { // this user. TODO: consider disabling it. HasEncryptionKeys bool `json:"has_encryption_keys"` - PasswordRecoveryHint string `json:"password_recovery_hint"` + AccountRecoveryHint string `json:"account_recovery_hint"` AppSpecificPasswords []*AppSpecificPasswordInfo `json:"app_specific_passwords,omitempty"` @@ -203,7 +203,7 @@ func (u *RawUser) setPrimaryPassword(ctx context.Context, tx TX, unlockPassword, // Set the password recovery hint for the user. When encryption keys are // present, requires a valid unlockPassword. -func (u *RawUser) setPasswordRecoveryHint(ctx context.Context, tx TX, unlockPassword, hint, response string) error { +func (u *RawUser) setAccountRecoveryHint(ctx context.Context, tx TX, unlockPassword, hint, response string) error { if u.HasEncryptionKeys { l, err := u.Keys.add(UserEncryptionKeyRecoveryID, unlockPassword, response) if err != nil { @@ -217,7 +217,7 @@ func (u *RawUser) setPasswordRecoveryHint(ctx context.Context, tx TX, unlockPass enc := pwhash.Encrypt(response) u.RecoveryPassword = enc - return tx.SetPasswordRecoveryHint(ctx, &u.User, hint, enc) + return tx.SetAccountRecoveryHint(ctx, &u.User, hint, enc) } // Initialize encryption keys for this user, given the primary authentication @@ -232,11 +232,11 @@ func (u *RawUser) initEncryption(ctx context.Context, tx TX, password string) er return err } } - if u.PasswordRecoveryHint != "" { - if err := tx.DeletePasswordRecoveryHint(ctx, &u.User); err != nil { + if u.AccountRecoveryHint != "" { + if err := tx.DeleteAccountRecoveryHint(ctx, &u.User); err != nil { return err } - u.PasswordRecoveryHint = "" + u.AccountRecoveryHint = "" } // Initialize a new key storage with the given primary password.