generated from VLADIMIR/template
add change password
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"evening_detective_server/internal/modules/processor_jwt"
|
||||
"evening_detective_server/internal/repos"
|
||||
"evening_detective_server/internal/repos/users_repo"
|
||||
"evening_detective_server/internal/services/users_service"
|
||||
"evening_detective_server/proto"
|
||||
"fmt"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
"net"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChangeUserPasswordAccess(t *testing.T) {
|
||||
s := &server{}
|
||||
for _, tc := range []struct {
|
||||
ctx context.Context
|
||||
want codes.Code
|
||||
}{
|
||||
{context.Background(), codes.Unauthenticated},
|
||||
{ctxWithClaims(), codes.PermissionDenied},
|
||||
{ctxWithClaims("user"), codes.PermissionDenied},
|
||||
{ctxWithClaims("author"), codes.PermissionDenied},
|
||||
{ctxWithClaims("organizer"), codes.PermissionDenied},
|
||||
} {
|
||||
_, err := s.ChangeUserPassword(tc.ctx, &proto.ChangeUserPasswordReq{Id: 2, NewPassword: "123456789012"})
|
||||
if status.Code(err) != tc.want {
|
||||
t.Errorf("code = %v, want %v", status.Code(err), tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Embed the real repository so unexpected calls fail rather than silently pass.
|
||||
type passwordRepo struct {
|
||||
*users_repo.UsersRepo
|
||||
role string
|
||||
saveErr error
|
||||
hash string
|
||||
}
|
||||
|
||||
func (r *passwordRepo) GetUserByID(_ context.Context, id int) (*repos.User, error) {
|
||||
if id == 1 {
|
||||
return &repos.User{ID: 1, Roles: []string{r.role}}, nil
|
||||
}
|
||||
if id == 2 {
|
||||
return &repos.User{ID: 2, Email: "target@example.org"}, nil
|
||||
}
|
||||
return nil, users_repo.ErrUserNotFound
|
||||
}
|
||||
func (r *passwordRepo) UpdateUserPassword(_ context.Context, email, hash string) error {
|
||||
if r.saveErr != nil {
|
||||
return r.saveErr
|
||||
}
|
||||
r.hash = hash
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestChangeUserPasswordTransports(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
jwt := processor_jwt.NewProcessor("test-secret")
|
||||
repo := &passwordRepo{role: "admin"}
|
||||
svc := users_service.NewUsersService(repo, nil, nil, nil, nil, "")
|
||||
listener := bufconn.Listen(1024 * 1024)
|
||||
defer listener.Close()
|
||||
gs := grpc.NewServer(grpc.UnaryInterceptor(processor_jwt.NewAuthorizationInterceptor(nil, jwt)))
|
||||
proto.RegisterEveningDetectiveServerServer(gs, &server{usersService: svc})
|
||||
go gs.Serve(listener)
|
||||
defer gs.Stop()
|
||||
conn, err := grpc.NewClient("passthrough:///test", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() }))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
mux := runtime.NewServeMux()
|
||||
if err := proto.RegisterEveningDetectiveServerHandler(ctx, mux, conn); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client := proto.NewEveningDetectiveServerClient(conn)
|
||||
for _, tc := range []struct {
|
||||
name, tokenRole, dbRole string
|
||||
id int32
|
||||
password string
|
||||
saveErr error
|
||||
code codes.Code
|
||||
httpCode int
|
||||
}{
|
||||
{"anonymous", "", "admin", 2, "123456789012", nil, codes.Unauthenticated, 401},
|
||||
{"user", "user", "admin", 2, "123456789012", nil, codes.PermissionDenied, 403},
|
||||
{"removed admin", "admin", "user", 2, "123456789012", nil, codes.PermissionDenied, 403},
|
||||
{"invalid", "admin", "admin", 2, "short", nil, codes.InvalidArgument, 400},
|
||||
{"missing", "admin", "admin", 3, "123456789012", nil, codes.NotFound, 404},
|
||||
{"save failure", "admin", "admin", 2, "123456789012", errors.New("private database detail"), codes.Internal, 500},
|
||||
{"deleted during save", "admin", "admin", 2, "123456789012", users_repo.ErrUserNotFound, codes.NotFound, 404},
|
||||
{"success", "admin", "admin", 2, "123456789012", nil, codes.OK, 200},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
repo.role, repo.saveErr, repo.hash = tc.dbRole, tc.saveErr, ""
|
||||
token := ""
|
||||
rpcCtx := ctx
|
||||
if tc.tokenRole != "" {
|
||||
token, err = jwt.GenerateAccessToken(1, "admin@example.org", []string{tc.tokenRole})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rpcCtx = metadata.NewOutgoingContext(ctx, metadata.Pairs("authorization", "Bearer "+token))
|
||||
}
|
||||
_, err := client.ChangeUserPassword(rpcCtx, &proto.ChangeUserPasswordReq{Id: tc.id, NewPassword: tc.password})
|
||||
if status.Code(err) != tc.code {
|
||||
t.Fatalf("gRPC = %v, want %v", err, tc.code)
|
||||
}
|
||||
req := httptest.NewRequest("POST", fmt.Sprintf("/api/users/%d/password", tc.id), strings.NewReader(fmt.Sprintf(`{"new_password":%q}`, tc.password)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
rsp := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rsp, req)
|
||||
if rsp.Code != tc.httpCode {
|
||||
t.Fatalf("HTTP = %d: %s", rsp.Code, rsp.Body.String())
|
||||
}
|
||||
if strings.Contains(rsp.Body.String(), tc.password) || strings.Contains(rsp.Body.String(), "private database detail") {
|
||||
t.Fatal("sensitive response")
|
||||
}
|
||||
if tc.code == codes.OK {
|
||||
if bcrypt.CompareHashAndPassword([]byte(repo.hash), []byte(tc.password)) != nil {
|
||||
t.Fatal("password not saved")
|
||||
}
|
||||
if _, err := jwt.GetClaims(token); err != nil {
|
||||
t.Fatal("existing token rejected")
|
||||
}
|
||||
} else if repo.hash != "" {
|
||||
t.Fatal("unauthorized or failed write")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"evening_detective_server/internal/modules/scenario_archive"
|
||||
"evening_detective_server/internal/modules/string_tools"
|
||||
"evening_detective_server/internal/repos/scenarios_repo"
|
||||
"evening_detective_server/internal/repos/users_repo"
|
||||
"evening_detective_server/internal/services/file_service"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"evening_detective_server/internal/services/scenarios_service"
|
||||
@@ -1226,3 +1227,25 @@ func (s *server) SetTeamAnswerScore(ctx context.Context, req *proto.SetTeamAnswe
|
||||
|
||||
return &proto.SetTeamAnswerScoreRsp{}, nil
|
||||
}
|
||||
|
||||
func (s *server) ChangeUserPassword(ctx context.Context, req *proto.ChangeUserPasswordReq) (*proto.ChangeUserPasswordRsp, error) {
|
||||
claims, ok := ctx.Value("claims").(*processor_jwt.JWTClaims)
|
||||
if !ok || claims == nil {
|
||||
return nil, status.Error(codes.Unauthenticated, "Требуется авторизация")
|
||||
}
|
||||
if !roles.HasRole(claims, roles.Admin) {
|
||||
return nil, status.Error(codes.PermissionDenied, "Недостаточно прав")
|
||||
}
|
||||
err := s.usersService.ChangeUserPassword(ctx, claims.UserID, int(req.GetId()), req.GetNewPassword())
|
||||
switch {
|
||||
case errors.Is(err, users_service.ErrPasswordChangeForbidden):
|
||||
return nil, status.Error(codes.PermissionDenied, err.Error())
|
||||
case errors.Is(err, users_service.ErrInvalidPasswordChange):
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Is(err, users_repo.ErrUserNotFound):
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
case err != nil:
|
||||
return nil, status.Error(codes.Internal, "Не удалось изменить пароль")
|
||||
}
|
||||
return &proto.ChangeUserPasswordRsp{}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user