User password change (#280)
* start passwordChangeHandler * add user scope * add user module / api path * add password change request * make comment clearer * add user to processor * required true * add processor call to handler * don't pass tc or channel * change password func + tests * add some first docs about password management * update swagger docs * add api tests * go fmt * test fixes
This commit is contained in:
parent
a07e62e49e
commit
107685e22e
|
@ -3362,6 +3362,51 @@ paths:
|
||||||
summary: See public statuses/posts that your instance is aware of.
|
summary: See public statuses/posts that your instance is aware of.
|
||||||
tags:
|
tags:
|
||||||
- timelines
|
- timelines
|
||||||
|
/api/v1/user/password_change:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- application/xml
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
description: |-
|
||||||
|
The parameters can also be given in the body of the request, as JSON, if the content-type is set to 'application/json'.
|
||||||
|
The parameters can also be given in the body of the request, as XML, if the content-type is set to 'application/xml'.
|
||||||
|
operationId: userPasswordChange
|
||||||
|
parameters:
|
||||||
|
- description: User's previous password.
|
||||||
|
in: formData
|
||||||
|
name: old_password
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
x-go-name: OldPassword
|
||||||
|
- description: |-
|
||||||
|
Desired new password.
|
||||||
|
If the password does not have high enough entropy, it will be rejected.
|
||||||
|
See https://github.com/wagslane/go-password-validator
|
||||||
|
in: formData
|
||||||
|
name: new_password
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
x-go-name: NewPassword
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Change successful
|
||||||
|
"400":
|
||||||
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
"403":
|
||||||
|
description: forbidden
|
||||||
|
"500":
|
||||||
|
description: internal error
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- write:user
|
||||||
|
summary: Change the password of authenticated user.
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
/users/{username}/statuses/{status}/replies:
|
/users/{username}/statuses/{status}/replies:
|
||||||
get:
|
get:
|
||||||
description: |-
|
description: |-
|
||||||
|
@ -3437,12 +3482,14 @@ securityDefinitions:
|
||||||
read:search: grant read access to searches
|
read:search: grant read access to searches
|
||||||
read:statuses: grants read access to statuses
|
read:statuses: grants read access to statuses
|
||||||
read:streaming: grants read access to streaming api
|
read:streaming: grants read access to streaming api
|
||||||
|
read:user: grants read access to user-level info
|
||||||
write: grants write access to everything
|
write: grants write access to everything
|
||||||
write:accounts: grants write access to accounts
|
write:accounts: grants write access to accounts
|
||||||
write:blocks: grants write access to blocks
|
write:blocks: grants write access to blocks
|
||||||
write:follows: grants write access to follows
|
write:follows: grants write access to follows
|
||||||
write:media: grants write access to media
|
write:media: grants write access to media
|
||||||
write:statuses: grants write access to statuses
|
write:statuses: grants write access to statuses
|
||||||
|
write:user: grants write access to user-level info
|
||||||
tokenUrl: https://example.org/oauth/token
|
tokenUrl: https://example.org/oauth/token
|
||||||
type: oauth2
|
type: oauth2
|
||||||
swagger: "2.0"
|
swagger: "2.0"
|
||||||
|
|
|
@ -40,12 +40,14 @@
|
||||||
// read:search: grant read access to searches
|
// read:search: grant read access to searches
|
||||||
// read:statuses: grants read access to statuses
|
// read:statuses: grants read access to statuses
|
||||||
// read:streaming: grants read access to streaming api
|
// read:streaming: grants read access to streaming api
|
||||||
|
// read:user: grants read access to user-level info
|
||||||
// write: grants write access to everything
|
// write: grants write access to everything
|
||||||
// write:accounts: grants write access to accounts
|
// write:accounts: grants write access to accounts
|
||||||
// write:blocks: grants write access to blocks
|
// write:blocks: grants write access to blocks
|
||||||
// write:follows: grants write access to follows
|
// write:follows: grants write access to follows
|
||||||
// write:media: grants write access to media
|
// write:media: grants write access to media
|
||||||
// write:statuses: grants write access to statuses
|
// write:statuses: grants write access to statuses
|
||||||
|
// write:user: grants write access to user-level info
|
||||||
// admin: grants admin access to everything
|
// admin: grants admin access to everything
|
||||||
// admin:accounts: grants admin access to accounts
|
// admin:accounts: grants admin access to accounts
|
||||||
// OAuth2 Application:
|
// OAuth2 Application:
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Password Management
|
||||||
|
|
||||||
|
GoToSocial stores hashes of user passwords in its database using the secure [bcrypt](https://en.wikipedia.org/wiki/Bcrypt) function in the [Go standard libraries](https://pkg.go.dev/golang.org/x/crypto/bcrypt).
|
||||||
|
|
||||||
|
This means that the plaintext value of your password is safe even if the database of your GoToSocial instance is compromised. It also means that your instance admin does not have access to your password.
|
||||||
|
|
||||||
|
To check whether a password is sufficiently secure before accepting it, GoToSocial uses [this library](https://github.com/wagslane/go-password-validator) with entropy set to 60. This means that passwords like `password` are rejected, but something like `verylongandsecurepasswordhahaha` would be accepted, even without special characters/upper+lowercase etc.
|
||||||
|
|
||||||
|
We recommend following the EFF's guidelines on [creating strong passwords](https://ssd.eff.org/en/module/creating-strong-passwords).
|
||||||
|
|
||||||
|
## Change Your Password
|
||||||
|
|
||||||
|
### API method
|
||||||
|
|
||||||
|
If you are logged in (ie., you have a valid oauth token), you can change your password by making a POST request to `/api/v1/user/password_change`, using your token as authentication, and giving your old password and desired new password as parameters. Check the [API documentation](../api/swagger.md) for more details.
|
||||||
|
|
||||||
|
## Reset Your Password
|
||||||
|
|
||||||
|
todo
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PasswordChangePOSTHandler swagger:operation POST /api/v1/user/password_change userPasswordChange
|
||||||
|
//
|
||||||
|
// Change the password of authenticated user.
|
||||||
|
//
|
||||||
|
// The parameters can also be given in the body of the request, as JSON, if the content-type is set to 'application/json'.
|
||||||
|
// The parameters can also be given in the body of the request, as XML, if the content-type is set to 'application/xml'.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - user
|
||||||
|
//
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// - application/xml
|
||||||
|
// - application/x-www-form-urlencoded
|
||||||
|
//
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// security:
|
||||||
|
// - OAuth2 Bearer:
|
||||||
|
// - write:user
|
||||||
|
//
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Change successful
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
|
// '400':
|
||||||
|
// description: bad request
|
||||||
|
// '500':
|
||||||
|
// description: "internal error"
|
||||||
|
func (m *Module) PasswordChangePOSTHandler(c *gin.Context) {
|
||||||
|
l := logrus.WithField("func", "PasswordChangePOSTHandler")
|
||||||
|
|
||||||
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
|
if err != nil {
|
||||||
|
l.Debugf("error authing: %s", err)
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check this user/account is active.
|
||||||
|
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
||||||
|
l.Debugf("couldn't auth: %s", err)
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := &model.PasswordChangeRequest{}
|
||||||
|
if err := c.ShouldBind(form); err != nil || form == nil || form.NewPassword == "" || form.OldPassword == "" {
|
||||||
|
if err != nil {
|
||||||
|
l.Debugf("could not parse form from request: %s", err)
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errWithCode := m.processor.UserChangePassword(c.Request.Context(), authed, form); errWithCode != nil {
|
||||||
|
l.Debugf("error changing user password: %s", errWithCode.Error())
|
||||||
|
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/user"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PasswordChangeTestSuite struct {
|
||||||
|
UserStandardTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() {
|
||||||
|
t := suite.testTokens["local_account_1"]
|
||||||
|
oauthToken := oauth.DBTokenToToken(t)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
|
||||||
|
ctx.Request.Form = url.Values{
|
||||||
|
"old_password": {"password"},
|
||||||
|
"new_password": {"peepeepoopoopassword"},
|
||||||
|
}
|
||||||
|
suite.userModule.PasswordChangePOSTHandler(ctx)
|
||||||
|
|
||||||
|
// check response
|
||||||
|
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
dbUser := >smodel.User{}
|
||||||
|
err := suite.db.GetByID(context.Background(), suite.testUsers["local_account_1"].ID, dbUser)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
// new password should pass
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("peepeepoopoopassword"))
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
// old password should fail
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("password"))
|
||||||
|
suite.EqualError(err, "crypto/bcrypt: hashedPassword is not the hash of the given password")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() {
|
||||||
|
t := suite.testTokens["local_account_1"]
|
||||||
|
oauthToken := oauth.DBTokenToToken(t)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
|
||||||
|
ctx.Request.Form = url.Values{
|
||||||
|
"new_password": {"peepeepoopoopassword"},
|
||||||
|
}
|
||||||
|
suite.userModule.PasswordChangePOSTHandler(ctx)
|
||||||
|
|
||||||
|
// check response
|
||||||
|
suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(`{"error":"missing one or more required form values"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
|
||||||
|
t := suite.testTokens["local_account_1"]
|
||||||
|
oauthToken := oauth.DBTokenToToken(t)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
|
||||||
|
ctx.Request.Form = url.Values{
|
||||||
|
"old_password": {"notright"},
|
||||||
|
"new_password": {"peepeepoopoopassword"},
|
||||||
|
}
|
||||||
|
suite.userModule.PasswordChangePOSTHandler(ctx)
|
||||||
|
|
||||||
|
// check response
|
||||||
|
suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(`{"error":"bad request: old password did not match"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
|
||||||
|
t := suite.testTokens["local_account_1"]
|
||||||
|
oauthToken := oauth.DBTokenToToken(t)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
|
||||||
|
ctx.Request.Form = url.Values{
|
||||||
|
"old_password": {"password"},
|
||||||
|
"new_password": {"peepeepoopoo"},
|
||||||
|
}
|
||||||
|
suite.userModule.PasswordChangePOSTHandler(ctx)
|
||||||
|
|
||||||
|
// check response
|
||||||
|
suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(`{"error":"bad request: insecure password, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPasswordChangeTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &PasswordChangeTestSuite{})
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BasePath is the base URI path for this module
|
||||||
|
BasePath = "/api/v1/user"
|
||||||
|
// PasswordChangePath is the path for POSTing a password change request.
|
||||||
|
PasswordChangePath = BasePath + "/password_change"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Module implements the ClientAPIModule interface
|
||||||
|
type Module struct {
|
||||||
|
config *config.Config
|
||||||
|
processor processing.Processor
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new user module
|
||||||
|
func New(config *config.Config, processor processing.Processor) api.ClientModule {
|
||||||
|
return &Module{
|
||||||
|
config: config,
|
||||||
|
processor: processor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route attaches all routes from this module to the given router
|
||||||
|
func (m *Module) Route(r router.Router) error {
|
||||||
|
r.AttachHandler(http.MethodPost, PasswordChangePath, m.PasswordChangePOSTHandler)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.iim.gay/grufwub/go-store/kv"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/user"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserStandardTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
config *config.Config
|
||||||
|
db db.DB
|
||||||
|
tc typeutils.TypeConverter
|
||||||
|
federator federation.Federator
|
||||||
|
processor processing.Processor
|
||||||
|
storage *kv.KVStore
|
||||||
|
|
||||||
|
testTokens map[string]*gtsmodel.Token
|
||||||
|
testClients map[string]*gtsmodel.Client
|
||||||
|
testApplications map[string]*gtsmodel.Application
|
||||||
|
testUsers map[string]*gtsmodel.User
|
||||||
|
testAccounts map[string]*gtsmodel.Account
|
||||||
|
|
||||||
|
userModule *user.Module
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UserStandardTestSuite) SetupTest() {
|
||||||
|
suite.testTokens = testrig.NewTestTokens()
|
||||||
|
suite.testClients = testrig.NewTestClients()
|
||||||
|
suite.testApplications = testrig.NewTestApplications()
|
||||||
|
suite.testUsers = testrig.NewTestUsers()
|
||||||
|
suite.testAccounts = testrig.NewTestAccounts()
|
||||||
|
suite.config = testrig.NewTestConfig()
|
||||||
|
suite.db = testrig.NewTestDB()
|
||||||
|
suite.storage = testrig.NewTestStorage()
|
||||||
|
testrig.InitTestLog()
|
||||||
|
suite.tc = testrig.NewTestTypeConverter(suite.db)
|
||||||
|
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)
|
||||||
|
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
||||||
|
suite.userModule = user.New(suite.config, suite.processor).(*user.Module)
|
||||||
|
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
||||||
|
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UserStandardTestSuite) TearDownTest() {
|
||||||
|
testrig.StandardDBTeardown(suite.db)
|
||||||
|
testrig.StandardStorageTeardown(suite.storage)
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
// PasswordChangeRequest models user password change parameters.
|
||||||
|
//
|
||||||
|
// swagger:parameters userPasswordChange
|
||||||
|
type PasswordChangeRequest struct {
|
||||||
|
// User's previous password.
|
||||||
|
//
|
||||||
|
// in: formData
|
||||||
|
// required: true
|
||||||
|
OldPassword string `form:"old_password" json:"old_password" xml:"old_password" validation:"required"`
|
||||||
|
// Desired new password.
|
||||||
|
// If the password does not have high enough entropy, it will be rejected.
|
||||||
|
// See https://github.com/wagslane/go-password-validator
|
||||||
|
//
|
||||||
|
// in: formData
|
||||||
|
// required: true
|
||||||
|
NewPassword string `form:"new_password" json:"new_password" xml:"new_password" validation:"required"`
|
||||||
|
}
|
|
@ -39,6 +39,7 @@ import (
|
||||||
mediaProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
mediaProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/streaming"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/streaming"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing/user"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/stream"
|
"github.com/superseriousbusiness/gotosocial/internal/stream"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/timeline"
|
"github.com/superseriousbusiness/gotosocial/internal/timeline"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
|
@ -173,6 +174,9 @@ type Processor interface {
|
||||||
// OpenStreamForAccount opens a new stream for the given account, with the given stream type.
|
// OpenStreamForAccount opens a new stream for the given account, with the given stream type.
|
||||||
OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*stream.Stream, gtserror.WithCode)
|
OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*stream.Stream, gtserror.WithCode)
|
||||||
|
|
||||||
|
// UserChangePassword changes the password for the given user, with the given form.
|
||||||
|
UserChangePassword(ctx context.Context, authed *oauth.Auth, form *apimodel.PasswordChangeRequest) gtserror.WithCode
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FEDERATION API-FACING PROCESSING FUNCTIONS
|
FEDERATION API-FACING PROCESSING FUNCTIONS
|
||||||
These functions are intended to be called when the federating client needs an immediate (ie., synchronous) reply
|
These functions are intended to be called when the federating client needs an immediate (ie., synchronous) reply
|
||||||
|
@ -247,6 +251,7 @@ type processor struct {
|
||||||
statusProcessor status.Processor
|
statusProcessor status.Processor
|
||||||
streamingProcessor streaming.Processor
|
streamingProcessor streaming.Processor
|
||||||
mediaProcessor mediaProcessor.Processor
|
mediaProcessor mediaProcessor.Processor
|
||||||
|
userProcessor user.Processor
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProcessor returns a new Processor that uses the given federator
|
// NewProcessor returns a new Processor that uses the given federator
|
||||||
|
@ -259,6 +264,7 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f
|
||||||
accountProcessor := account.New(db, tc, mediaHandler, oauthServer, fromClientAPI, federator, config)
|
accountProcessor := account.New(db, tc, mediaHandler, oauthServer, fromClientAPI, federator, config)
|
||||||
adminProcessor := admin.New(db, tc, mediaHandler, fromClientAPI, config)
|
adminProcessor := admin.New(db, tc, mediaHandler, fromClientAPI, config)
|
||||||
mediaProcessor := mediaProcessor.New(db, tc, mediaHandler, storage, config)
|
mediaProcessor := mediaProcessor.New(db, tc, mediaHandler, storage, config)
|
||||||
|
userProcessor := user.New(db, config)
|
||||||
|
|
||||||
return &processor{
|
return &processor{
|
||||||
fromClientAPI: fromClientAPI,
|
fromClientAPI: fromClientAPI,
|
||||||
|
@ -279,6 +285,7 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f
|
||||||
statusProcessor: statusProcessor,
|
statusProcessor: statusProcessor,
|
||||||
streamingProcessor: streamingProcessor,
|
streamingProcessor: streamingProcessor,
|
||||||
mediaProcessor: mediaProcessor,
|
mediaProcessor: mediaProcessor,
|
||||||
|
userProcessor: userProcessor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) UserChangePassword(ctx context.Context, authed *oauth.Auth, form *apimodel.PasswordChangeRequest) gtserror.WithCode {
|
||||||
|
return p.userProcessor.ChangePassword(ctx, authed.User, form.OldPassword, form.NewPassword)
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) ChangePassword(ctx context.Context, user *gtsmodel.User, oldPassword string, newPassword string) gtserror.WithCode {
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(oldPassword)); err != nil {
|
||||||
|
return gtserror.NewErrorBadRequest(err, "old password did not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validate.NewPassword(newPassword); err != nil {
|
||||||
|
return gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return gtserror.NewErrorInternalError(err, "error hashing password")
|
||||||
|
}
|
||||||
|
|
||||||
|
user.EncryptedPassword = string(newPasswordHash)
|
||||||
|
if err := p.db.UpdateByPrimaryKey(ctx, user); err != nil {
|
||||||
|
return gtserror.NewErrorInternalError(err, "database error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChangePasswordTestSuite struct {
|
||||||
|
UserStandardTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ChangePasswordTestSuite) TestChangePasswordOK() {
|
||||||
|
user := suite.testUsers["local_account_1"]
|
||||||
|
|
||||||
|
errWithCode := suite.user.ChangePassword(context.Background(), user, "password", "verygoodnewpassword")
|
||||||
|
suite.NoError(errWithCode)
|
||||||
|
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte("verygoodnewpassword"))
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
// get user from the db again
|
||||||
|
dbUser := >smodel.User{}
|
||||||
|
err = suite.db.GetByID(context.Background(), user.ID, dbUser)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
// check the password has changed
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("verygoodnewpassword"))
|
||||||
|
suite.NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ChangePasswordTestSuite) TestChangePasswordIncorrectOld() {
|
||||||
|
user := suite.testUsers["local_account_1"]
|
||||||
|
|
||||||
|
errWithCode := suite.user.ChangePassword(context.Background(), user, "ooooopsydoooopsy", "verygoodnewpassword")
|
||||||
|
suite.EqualError(errWithCode, "crypto/bcrypt: hashedPassword is not the hash of the given password")
|
||||||
|
suite.Equal(http.StatusBadRequest, errWithCode.Code())
|
||||||
|
suite.Equal("bad request: old password did not match", errWithCode.Safe())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ChangePasswordTestSuite) TestChangePasswordWeakNew() {
|
||||||
|
user := suite.testUsers["local_account_1"]
|
||||||
|
|
||||||
|
errWithCode := suite.user.ChangePassword(context.Background(), user, "password", "1234")
|
||||||
|
suite.EqualError(errWithCode, "insecure password, try including more special characters, using lowercase letters, using uppercase letters or using a longer password")
|
||||||
|
suite.Equal(http.StatusBadRequest, errWithCode.Code())
|
||||||
|
suite.Equal("bad request: insecure password, try including more special characters, using lowercase letters, using uppercase letters or using a longer password", errWithCode.Safe())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChangePasswordTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &ChangePasswordTestSuite{})
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Processor wraps a bunch of functions for processing user-level actions.
|
||||||
|
type Processor interface {
|
||||||
|
// ChangePassword changes the specified user's password from old => new,
|
||||||
|
// or returns an error if the new password is too weak, or the old password is incorrect.
|
||||||
|
ChangePassword(ctx context.Context, user *gtsmodel.User, oldPassword string, newPassword string) gtserror.WithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
type processor struct {
|
||||||
|
config *config.Config
|
||||||
|
db db.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new user processor
|
||||||
|
func New(db db.DB, config *config.Config) Processor {
|
||||||
|
return &processor{
|
||||||
|
config: config,
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing/user"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserStandardTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
config *config.Config
|
||||||
|
db db.DB
|
||||||
|
|
||||||
|
testUsers map[string]*gtsmodel.User
|
||||||
|
|
||||||
|
user user.Processor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UserStandardTestSuite) SetupTest() {
|
||||||
|
testrig.InitTestLog()
|
||||||
|
suite.config = testrig.NewTestConfig()
|
||||||
|
suite.db = testrig.NewTestDB()
|
||||||
|
suite.testUsers = testrig.NewTestUsers()
|
||||||
|
suite.user = user.New(suite.db, suite.config)
|
||||||
|
|
||||||
|
testrig.StandardDBSetup(suite.db, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UserStandardTestSuite) TearDownTest() {
|
||||||
|
testrig.StandardDBTeardown(suite.db)
|
||||||
|
}
|
Loading…
Reference in New Issue