From af4fe0c6be6d66a9dd57535f766c9ac95d80a6f4 Mon Sep 17 00:00:00 2001 From: ale <ale@incal.net> Date: Tue, 26 Jun 2018 20:14:59 +0100 Subject: [PATCH] Add an API endpoint to reset a resource password Also set an initial user password, and random passwords for all its resources, on a CreateUser request. --- actions.go | 104 ++++++++++++++++++++++++++++++++++++++++++++--- actions_test.go | 26 ++++++++++++ server/server.go | 8 ++++ 3 files changed, 132 insertions(+), 6 deletions(-) diff --git a/actions.go b/actions.go index 440e40c1..6e2ed414 100644 --- a/actions.go +++ b/actions.go @@ -10,6 +10,7 @@ import ( "git.autistici.org/ai3/go-common/pwhash" "github.com/pquerna/otp/totp" + "github.com/sethvargo/go-password/password" ) // RequestBase contains parameters shared by all request types. @@ -421,6 +422,71 @@ func (s *AccountService) DeleteApplicationSpecificPassword(ctx context.Context, }) } +// ResetResourcePasswordRequest is the request type for AccountService.ResetResourcePassword(). +type ResetResourcePasswordRequest struct { + ResourceRequestBase +} + +func resourceHasPassword(r *Resource) bool { + switch r.ID.Type() { + case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList: + return true + default: + return false + } +} + +// Validate the request. +func (r *ResetResourcePasswordRequest) Validate(ctx context.Context, s *AccountService) error { + switch r.ResourceID.Type() { + case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList: + case ResourceTypeEmail: + return errors.New("can't reset email passwords independently") + default: + return errors.New("can't reset password on this resource type") + } + return nil +} + +// ResetResourcePasswordResponse is the response type for AccountService.ResetResourcePassword(). +type ResetResourcePasswordResponse struct { + Password string `json:"password"` +} + +// ResetResourcePassword can reset the password associated with a +// resource (if the resource type supports it). It will generate a +// random password and return it to the caller. +func (s *AccountService) ResetResourcePassword(ctx context.Context, tx TX, req *ResetResourcePasswordRequest) (*ResetResourcePasswordResponse, error) { + var resp ResetResourcePasswordResponse + err := s.handleResourceRequest(ctx, tx, req, s.authResource(req.ResourceRequestBase), func(ctx context.Context, r *Resource) error { + newPassword, err := s.doResetResourcePassword(ctx, tx, r) + if err != nil { + return err + } + + // Return the password to the caller, in cleartext. + resp.Password = newPassword + + // TODO: This is where we may want to call out to + // other backends in order to reset credentials for + // certain resources that have their own secondary + // authentication databases (lists, mysql). + return nil + }) + return &resp, err +} + +func (s *AccountService) doResetResourcePassword(ctx context.Context, tx TX, r *Resource) (string, error) { + newPassword := randomPassword() + encPassword := pwhash.Encrypt(newPassword) + + // TODO: this needs a resource type-switch. + if err := tx.SetResourcePassword(ctx, r, encPassword); err != nil { + return "", err + } + return newPassword, nil +} + // MoveResourceRequest is the request type for AccountService.MoveResource(). type MoveResourceRequest struct { RequestBase @@ -721,7 +787,8 @@ func (req *CreateUserRequest) Validate(ctx context.Context, s *AccountService, _ // CreateUserResponse is the request type for AccountService.CreateUser(). type CreateUserResponse struct { - User *User `json:"user,omitempty"` + User *User `json:"user,omitempty"` + Password string `json:"password"` } // Make sure that only user-settable fields are set in the User in a @@ -742,28 +809,53 @@ func fillUserTemplate(user *User) { func (s *AccountService) CreateUser(ctx context.Context, tx TX, req *CreateUserRequest) (*CreateUserResponse, error) { var resp CreateUserResponse err := s.handleAdminRequest(ctx, tx, req, req.SSO, func(ctx context.Context) (err error) { + // Create the user first, along with all the resources. if err := tx.CreateUser(ctx, req.User); err != nil { return err } resp.User = req.User + + // Now set a password for the user and return it, and + // set random passwords for all the resources + // (currently, we don't care about those, the user + // will reset them later). + newPassword := randomPassword() + if err := s.changeUserPasswordAndResetEncryptionKeys(ctx, tx, req.User, newPassword); err != nil { + return err + } + resp.Password = newPassword + + for _, r := range req.User.Resources { + if resourceHasPassword(r) { + if _, err := s.doResetResourcePassword(ctx, tx, r); err != nil { + // Just log, don't fail. + log.Printf("can't set random password for resource %s: %v", r.ID, err) + } + } + } + return nil }) return &resp, err } -const appSpecificPasswordLen = 64 - func randomBase64(n int) string { - b := make([]byte, n/4*3) + b := make([]byte, n*3/4) _, err := rand.Read(b[:]) if err != nil { panic(err) } - return base64.StdEncoding.EncodeToString(b[:]) + return base64.StdEncoding.EncodeToString(b[:])[:n] +} + +func randomPassword() string { + // Create a 16-character password with 4 digits and 2 symbols. + return password.MustGenerate(16, 4, 2, false, false) } func randomAppSpecificPassword() string { - return randomBase64(appSpecificPasswordLen) + // Create a 64-character password with 10 digits and 10 symbols. + return password.MustGenerate(64, 10, 10, false, false) } const appSpecificPasswordIDLen = 4 diff --git a/actions_test.go b/actions_test.go index ad0a6fa7..81cda078 100644 --- a/actions_test.go +++ b/actions_test.go @@ -61,6 +61,7 @@ func (b *fakeBackend) SetPasswordRecoveryHint(_ context.Context, user *User, hin } func (b *fakeBackend) SetResourcePassword(_ context.Context, r *Resource, password string) error { + b.passwords[r.ID.String()] = password return nil } @@ -473,3 +474,28 @@ func TestService_CreateUser_FailIfNotAdmin(t *testing.T) { t.Fatal("CreateResources did not fail") } } + +func TestService_ResetResourcePassword(t *testing.T) { + svc, tx := testService("") + id := NewResourceID(ResourceTypeDAV, "testuser", "dav1") + req := &ResetResourcePasswordRequest{ + ResourceRequestBase: ResourceRequestBase{ + SSO: "testuser", + ResourceID: id, + }, + } + resp, err := svc.ResetResourcePassword(context.Background(), tx, req) + if err != nil { + t.Fatal("ResetResourcePassword", err) + } + if len(resp.Password) < 10 { + t.Fatalf("short password: %q", resp.Password) + } + storedPw, ok := tx.(*fakeBackend).passwords[id.String()] + if !ok { + t.Fatal("resource password was not actually set on the backend") + } + if storedPw == resp.Password { + t.Fatal("oops, it appears that the password was stored in cleartext on the backend") + } +} diff --git a/server/server.go b/server/server.go index 2b9a4692..06d58e49 100644 --- a/server/server.go +++ b/server/server.go @@ -100,6 +100,13 @@ func (s *AccountServer) handleMoveResource(tx as.TX, w http.ResponseWriter, r *h }) } +func (s *AccountServer) handleResetResourcePassword(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) { + var req as.ResetResourcePasswordRequest + return handleJSON(w, r, &req, func(ctx context.Context) (interface{}, error) { + return s.service.ResetResourcePassword(ctx, tx, &req) + }) +} + func (s *AccountServer) handleEnableOTP(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) { var req as.EnableOTPRequest return handleJSON(w, r, &req, func(ctx context.Context) (interface{}, error) { @@ -152,6 +159,7 @@ func (s *AccountServer) Handler() http.Handler { h.HandleFunc("/api/resource/disable", s.withTx(s.handleDisableResource)) h.HandleFunc("/api/resource/create", s.withTx(s.handleCreateResources)) h.HandleFunc("/api/resource/move", s.withTx(s.handleMoveResource)) + h.HandleFunc("/api/resource/reset_password", s.withTx(s.handleResetResourcePassword)) h.HandleFunc("/api/recover_password", s.withTx(s.handleRecoverPassword)) return h } -- GitLab