Commit af4fe0c6 authored by ale's avatar ale
Browse files

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.
parent 11cab73f
......@@ -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
......
......@@ -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")
}
}
......@@ -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
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment