Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit2091628

Browse files
authored
feat: Add reset-password command (#1380)
* allow non-destructively checking if database needs to be migrated* feat: Add reset-password command* fix linter errors* clean up reset-password usage prompt* Add confirmation to reset-password command* Ping database before checking migration, to improve error message
1 parenta629a70 commit2091628

File tree

5 files changed

+325
-7
lines changed

5 files changed

+325
-7
lines changed

‎cli/resetpassword.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package cli
2+
3+
import (
4+
"database/sql"
5+
6+
"github.com/spf13/cobra"
7+
"golang.org/x/xerrors"
8+
9+
"github.com/coder/coder/cli/cliflag"
10+
"github.com/coder/coder/cli/cliui"
11+
"github.com/coder/coder/coderd/database"
12+
"github.com/coder/coder/coderd/userpassword"
13+
)
14+
15+
funcresetPassword()*cobra.Command {
16+
var (
17+
postgresURLstring
18+
)
19+
20+
root:=&cobra.Command{
21+
Use:"reset-password <username>",
22+
Short:"Reset a user's password by directly updating the database",
23+
Args:cobra.ExactArgs(1),
24+
RunE:func(cmd*cobra.Command,args []string)error {
25+
username:=args[0]
26+
27+
sqlDB,err:=sql.Open("postgres",postgresURL)
28+
iferr!=nil {
29+
returnxerrors.Errorf("dial postgres: %w",err)
30+
}
31+
defersqlDB.Close()
32+
err=sqlDB.Ping()
33+
iferr!=nil {
34+
returnxerrors.Errorf("ping postgres: %w",err)
35+
}
36+
37+
err=database.EnsureClean(sqlDB)
38+
iferr!=nil {
39+
returnxerrors.Errorf("database needs migration: %w",err)
40+
}
41+
db:=database.New(sqlDB)
42+
43+
user,err:=db.GetUserByEmailOrUsername(cmd.Context(), database.GetUserByEmailOrUsernameParams{
44+
Username:username,
45+
})
46+
iferr!=nil {
47+
returnxerrors.Errorf("retrieving user: %w",err)
48+
}
49+
50+
password,err:=cliui.Prompt(cmd, cliui.PromptOptions{
51+
Text:"Enter new "+cliui.Styles.Field.Render("password")+":",
52+
Secret:true,
53+
Validate:cliui.ValidateNotEmpty,
54+
})
55+
iferr!=nil {
56+
returnxerrors.Errorf("password prompt: %w",err)
57+
}
58+
confirmedPassword,err:=cliui.Prompt(cmd, cliui.PromptOptions{
59+
Text:"Confirm "+cliui.Styles.Field.Render("password")+":",
60+
Secret:true,
61+
Validate:cliui.ValidateNotEmpty,
62+
})
63+
iferr!=nil {
64+
returnxerrors.Errorf("confirm password prompt: %w",err)
65+
}
66+
ifpassword!=confirmedPassword {
67+
returnxerrors.New("Passwords do not match")
68+
}
69+
70+
hashedPassword,err:=userpassword.Hash(password)
71+
iferr!=nil {
72+
returnxerrors.Errorf("hash password: %w",err)
73+
}
74+
75+
err=db.UpdateUserHashedPassword(cmd.Context(), database.UpdateUserHashedPasswordParams{
76+
ID:user.ID,
77+
HashedPassword: []byte(hashedPassword),
78+
})
79+
iferr!=nil {
80+
returnxerrors.Errorf("updating password: %w",err)
81+
}
82+
83+
returnnil
84+
},
85+
}
86+
87+
cliflag.StringVarP(root.Flags(),&postgresURL,"postgres-url","","CODER_PG_CONNECTION_URL","","URL of a PostgreSQL database to connect to")
88+
89+
returnroot
90+
}

‎cli/resetpassword_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"net/url"
6+
"runtime"
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/coder/coder/cli/clitest"
13+
"github.com/coder/coder/coderd/database/postgres"
14+
"github.com/coder/coder/codersdk"
15+
"github.com/coder/coder/pty/ptytest"
16+
)
17+
18+
funcTestResetPassword(t*testing.T) {
19+
t.Parallel()
20+
21+
ifruntime.GOOS!="linux"||testing.Short() {
22+
// Skip on non-Linux because it spawns a PostgreSQL instance.
23+
t.SkipNow()
24+
}
25+
26+
constemail="some@one.com"
27+
constusername="example"
28+
constoldPassword="password"
29+
constnewPassword="password2"
30+
31+
// start postgres and coder server processes
32+
33+
connectionURL,closeFunc,err:=postgres.Open()
34+
require.NoError(t,err)
35+
defercloseFunc()
36+
ctx,cancelFunc:=context.WithCancel(context.Background())
37+
serverDone:=make(chanstruct{})
38+
serverCmd,cfg:=clitest.New(t,"server","--address",":0","--postgres-url",connectionURL)
39+
gofunc() {
40+
deferclose(serverDone)
41+
err=serverCmd.ExecuteContext(ctx)
42+
require.ErrorIs(t,err,context.Canceled)
43+
}()
44+
varclient*codersdk.Client
45+
require.Eventually(t,func()bool {
46+
rawURL,err:=cfg.URL().Read()
47+
iferr!=nil {
48+
returnfalse
49+
}
50+
accessURL,err:=url.Parse(rawURL)
51+
require.NoError(t,err)
52+
client=codersdk.New(accessURL)
53+
returntrue
54+
},15*time.Second,25*time.Millisecond)
55+
_,err=client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
56+
Email:email,
57+
Username:username,
58+
Password:oldPassword,
59+
OrganizationName:"example",
60+
})
61+
require.NoError(t,err)
62+
63+
// reset the password
64+
65+
resetCmd,cmdCfg:=clitest.New(t,"reset-password","--postgres-url",connectionURL,username)
66+
clitest.SetupConfig(t,client,cmdCfg)
67+
cmdDone:=make(chanstruct{})
68+
pty:=ptytest.New(t)
69+
resetCmd.SetIn(pty.Input())
70+
resetCmd.SetOut(pty.Output())
71+
gofunc() {
72+
deferclose(cmdDone)
73+
err=resetCmd.Execute()
74+
require.NoError(t,err)
75+
}()
76+
77+
matches:= []struct {
78+
outputstring
79+
inputstring
80+
}{
81+
{"Enter new",newPassword},
82+
{"Confirm",newPassword},
83+
}
84+
for_,match:=rangematches {
85+
pty.ExpectMatch(match.output)
86+
pty.WriteLine(match.input)
87+
}
88+
<-cmdDone
89+
90+
// now try logging in
91+
92+
_,err=client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
93+
Email:email,
94+
Password:oldPassword,
95+
})
96+
require.Error(t,err)
97+
98+
_,err=client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
99+
Email:email,
100+
Password:newPassword,
101+
})
102+
require.NoError(t,err)
103+
104+
cancelFunc()
105+
<-serverDone
106+
}

‎cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func Root() *cobra.Command {
6161
list(),
6262
login(),
6363
publickey(),
64+
resetPassword(),
6465
server(),
6566
show(),
6667
start(),

‎coderd/database/migrate.go

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,40 @@ import (
44
"database/sql"
55
"embed"
66
"errors"
7+
"os"
78

89
"github.com/golang-migrate/migrate/v4"
910
"github.com/golang-migrate/migrate/v4/database/postgres"
11+
"github.com/golang-migrate/migrate/v4/source"
1012
"github.com/golang-migrate/migrate/v4/source/iofs"
1113
"golang.org/x/xerrors"
1214
)
1315

1416
//go:embed migrations/*.sql
1517
varmigrations embed.FS
1618

17-
funcmigrateSetup(db*sql.DB) (*migrate.Migrate,error) {
19+
funcmigrateSetup(db*sql.DB) (source.Driver,*migrate.Migrate,error) {
1820
sourceDriver,err:=iofs.New(migrations,"migrations")
1921
iferr!=nil {
20-
returnnil,xerrors.Errorf("create iofs: %w",err)
22+
returnnil,nil,xerrors.Errorf("create iofs: %w",err)
2123
}
2224

2325
dbDriver,err:=postgres.WithInstance(db,&postgres.Config{})
2426
iferr!=nil {
25-
returnnil,xerrors.Errorf("wrap postgres connection: %w",err)
27+
returnnil,nil,xerrors.Errorf("wrap postgres connection: %w",err)
2628
}
2729

2830
m,err:=migrate.NewWithInstance("",sourceDriver,"",dbDriver)
2931
iferr!=nil {
30-
returnnil,xerrors.Errorf("new migrate instance: %w",err)
32+
returnnil,nil,xerrors.Errorf("new migrate instance: %w",err)
3133
}
3234

33-
returnm,nil
35+
returnsourceDriver,m,nil
3436
}
3537

3638
// MigrateUp runs SQL migrations to ensure the database schema is up-to-date.
3739
funcMigrateUp(db*sql.DB)error {
38-
m,err:=migrateSetup(db)
40+
_,m,err:=migrateSetup(db)
3941
iferr!=nil {
4042
returnxerrors.Errorf("migrate setup: %w",err)
4143
}
@@ -55,7 +57,7 @@ func MigrateUp(db *sql.DB) error {
5557

5658
// MigrateDown runs all down SQL migrations.
5759
funcMigrateDown(db*sql.DB)error {
58-
m,err:=migrateSetup(db)
60+
_,m,err:=migrateSetup(db)
5961
iferr!=nil {
6062
returnxerrors.Errorf("migrate setup: %w",err)
6163
}
@@ -72,3 +74,68 @@ func MigrateDown(db *sql.DB) error {
7274

7375
returnnil
7476
}
77+
78+
// EnsureClean checks whether all migrations for the current version have been
79+
// applied, without making any changes to the database. If not, returns a
80+
// non-nil error.
81+
funcEnsureClean(db*sql.DB)error {
82+
sourceDriver,m,err:=migrateSetup(db)
83+
iferr!=nil {
84+
returnxerrors.Errorf("migrate setup: %w",err)
85+
}
86+
87+
version,dirty,err:=m.Version()
88+
iferr!=nil {
89+
returnxerrors.Errorf("get migration version: %w",err)
90+
}
91+
92+
ifdirty {
93+
returnxerrors.Errorf("database has not been cleanly migrated")
94+
}
95+
96+
// Verify that the database's migration version is "current" by checking
97+
// that a migration with that version exists, but there is no next version.
98+
err=CheckLatestVersion(sourceDriver,version)
99+
iferr!=nil {
100+
returnxerrors.Errorf("database needs migration: %w",err)
101+
}
102+
103+
returnnil
104+
}
105+
106+
// Returns nil if currentVersion corresponds to the latest available migration,
107+
// otherwise an error explaining why not.
108+
funcCheckLatestVersion(sourceDriver source.Driver,currentVersionuint)error {
109+
// This is ugly, but seems like the only way to do it with the public
110+
// interfaces provided by golang-migrate.
111+
112+
// Check that there is no later version
113+
nextVersion,err:=sourceDriver.Next(currentVersion)
114+
iferr==nil {
115+
returnxerrors.Errorf("current version is %d, but later version %d exists",currentVersion,nextVersion)
116+
}
117+
if!errors.Is(err,os.ErrNotExist) {
118+
returnxerrors.Errorf("get next migration after %d: %w",currentVersion,err)
119+
}
120+
121+
// Once we reach this point, we know that either currentVersion doesn't
122+
// exist, or it has no successor (the return value from
123+
// sourceDriver.Next() is the same in either case). So we need to check
124+
// that either it's the first version, or it has a predecessor.
125+
126+
firstVersion,err:=sourceDriver.First()
127+
iferr!=nil {
128+
// the total number of migrations should be non-zero, so this must be
129+
// an actual error, not just a missing file
130+
returnxerrors.Errorf("get first migration: %w",err)
131+
}
132+
iffirstVersion==currentVersion {
133+
returnnil
134+
}
135+
136+
_,err=sourceDriver.Prev(currentVersion)
137+
iferr!=nil {
138+
returnxerrors.Errorf("get previous migration: %w",err)
139+
}
140+
returnnil
141+
}

‎coderd/database/migrate_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ package database_test
44

55
import (
66
"database/sql"
7+
"fmt"
78
"testing"
89

10+
"github.com/golang-migrate/migrate/v4/source"
11+
"github.com/golang-migrate/migrate/v4/source/stub"
912
"github.com/stretchr/testify/require"
1013
"go.uber.org/goleak"
1114

@@ -75,3 +78,54 @@ func testSQLDB(t testing.TB) *sql.DB {
7578

7679
returndb
7780
}
81+
82+
// paralleltest linter doesn't correctly handle table-driven tests (https://github.com/kunwardeep/paralleltest/issues/8)
83+
// nolint:paralleltest
84+
funcTestCheckLatestVersion(t*testing.T) {
85+
t.Parallel()
86+
87+
typeteststruct {
88+
currentVersionuint
89+
existingVersions []uint
90+
expectedResultstring
91+
}
92+
93+
tests:= []test{
94+
// successful cases
95+
{1, []uint{1},""},
96+
{3, []uint{1,2,3},""},
97+
{3, []uint{1,3},""},
98+
99+
// failure cases
100+
{1, []uint{1,2},"current version is 1, but later version 2 exists"},
101+
{2, []uint{1,2,3},"current version is 2, but later version 3 exists"},
102+
{4, []uint{1,2,3},"get previous migration: prev for version 4 : file does not exist"},
103+
{4, []uint{1,2,3,5},"get previous migration: prev for version 4 : file does not exist"},
104+
}
105+
106+
fori,tc:=rangetests {
107+
i,tc:=i,tc
108+
t.Run(fmt.Sprintf("entry %d",i),func(t*testing.T) {
109+
t.Parallel()
110+
111+
driver,_:=stub.WithInstance(nil,&stub.Config{})
112+
stub,ok:=driver.(*stub.Stub)
113+
require.True(t,ok)
114+
for_,version:=rangetc.existingVersions {
115+
stub.Migrations.Append(&source.Migration{
116+
Version:version,
117+
Identifier:"",
118+
Direction:source.Up,
119+
Raw:"",
120+
})
121+
}
122+
123+
err:=database.CheckLatestVersion(driver,tc.currentVersion)
124+
varerrMessagestring
125+
iferr!=nil {
126+
errMessage=err.Error()
127+
}
128+
require.Equal(t,tc.expectedResult,errMessage)
129+
})
130+
}
131+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp