Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement dropUser command #3866

Merged
merged 19 commits into from
Dec 20, 2023
Prev Previous commit
Next Next commit
wip
  • Loading branch information
henvic committed Dec 17, 2023
commit 5fcc3b00d64c24cd1b8b25f2dad50976ff4ce558
129 changes: 65 additions & 64 deletions integration/users/create_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ package users

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"

Expand All @@ -26,42 +29,39 @@ import (
"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/must"
"github.com/FerretDB/FerretDB/internal/util/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCreateUser(t *testing.T) {
t.Parallel()

ctx, collection := setup.Setup(t)

_ = collection.Database().RunCommand(ctx, bson.D{
{"dropAllUsersFromDatabase", 1},
})

db := collection.Database()
client := db.Client()
users := client.Database("admin").Collection("system.users")

// TODO https://github.com/FerretDB/FerretDB/issues/1492
if setup.IsMongoDB(t) {
assert.NoError(t, collection.Database().RunCommand(ctx, bson.D{
{"dropAllUsersFromDatabase", 1},
}).Err())
} else {
// Erase any previously saved user in the database.
_, err := users.DeleteMany(ctx, bson.D{{"db", db.Name()}})
assert.NoError(t, err)
}

err := db.RunCommand(ctx, bson.D{
{Key: "createUser", Value: "should_already_exist"},
{Key: "roles", Value: bson.A{}},
{Key: "pwd", Value: "password"},
}).Err()
assert.NoError(t, err)

testCases := map[string]struct {
testCases := map[string]struct { //nolint:vet // for readability
payload bson.D
err *mongo.CommandError
altMessage string
expected bson.D
skip string
}{
"AlreadyExists": {
skip: "TODO", // FIXME

payload: bson.D{
{Key: "createUser", Value: "should_already_exist"},
{Key: "roles", Value: bson.A{}},
{Key: "pwd", Value: "password"},
{"createUser", "should_already_exist"},
{"roles", bson.A{}},
{"pwd", "password"},
},
err: &mongo.CommandError{
Code: 51003,
Expand All @@ -70,11 +70,9 @@ func TestCreateUser(t *testing.T) {
},
},
"MissingPwdOrExternal": {
skip: "TODO", // FIXME

payload: bson.D{
{Key: "createUser", Value: "mising_pwd_or_external"},
{Key: "roles", Value: bson.A{}},
{"createUser", "mising_pwd_or_external"},
{"roles", bson.A{}},
},
err: &mongo.CommandError{
Code: 2,
Expand All @@ -84,60 +82,51 @@ func TestCreateUser(t *testing.T) {
},
"Success": {
payload: bson.D{
{Key: "createUser", Value: "success_user"},
{Key: "roles", Value: bson.A{}},
{Key: "pwd", Value: "password"},
{"createUser", "success_user"},
{"roles", bson.A{}},
{"pwd", "password"},
},
expected: bson.D{
{
Key: "ok", Value: float64(1),
"ok", float64(1),
},
},
},
"WithComment": {
skip: "TODO", // FIXME

payload: bson.D{
{Key: "createUser", Value: "with_comment_user"},
{Key: "roles", Value: bson.A{}},
{Key: "pwd", Value: "password"},
{Key: "comment", Value: "test string comment"},
{"createUser", "with_comment_user"},
{"roles", bson.A{}},
{"pwd", "password"},
{"comment", "test string comment"},
},
expected: bson.D{
{
Key: "ok", Value: float64(1),
"ok", float64(1),
},
},
},
"WithCommentComposite": {
skip: "TODO", // FIXME

"MissingRoles": {
payload: bson.D{
{Key: "createUser", Value: "with_comment_composite"},
{Key: "roles", Value: bson.A{}},
{Key: "pwd", Value: "password"},
{
Key: "comment",
Value: bson.D{
{Key: "example", Value: "blah"},
{
Key: "complex",
Value: bson.A{
bson.D{{Key: "x", Value: "y"}},
},
},
},
},
{"createUser", "missing_roles"},
{"pwd", "password"},
},
expected: bson.D{
{
Key: "ok", Value: float64(1),
},
err: &mongo.CommandError{
Code: 40414,
Name: "Location40414",
Message: "BSON field 'createUser.roles' is missing but a required field",
},
},
}

err := db.RunCommand(ctx, bson.D{
{"createUser", "should_already_exist"},
{"roles", bson.A{}},
{"pwd", "password"},
}).Err()
assert.NoError(t, err)

for name, tc := range testCases {
tc := tc
name, tc := name, tc
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
Expand All @@ -162,23 +151,35 @@ func TestCreateUser(t *testing.T) {
testutil.AssertEqual(t, expected, actual)

payload := integration.ConvertDocument(t, tc.payload)
assertUserExists(ctx, t, db, payload)
assertUserExists(ctx, t, users, db.Name(), payload)
})
}
}

func assertUserExists(ctx context.Context, t testing.TB, db *mongo.Database, payload *types.Document) {
// assertUserExists checks it the user was created in the admin.system.users collection.
// All users are created in the "admin" database.
func assertUserExists(ctx context.Context, t testing.TB, users *mongo.Collection, dbName string, payload *types.Document) {
t.Helper()

var rec bson.D
err := db.Collection("system.users").FindOne(ctx, bson.D{{"user", must.NotFail(payload.Get("createUser"))}}).Decode(&rec)
require.NoError(t, err)
err := users.FindOne(ctx, bson.D{{"user", must.NotFail(payload.Get("createUser"))}}).Decode(&rec)
require.NoError(t, err, "user not found")

actualRecorded := integration.ConvertDocument(t, rec)

uuid := must.NotFail(actualRecorded.Get("userId")).(types.Binary)
assert.Equal(t, uuid.Subtype.String(), types.BinaryUUID.String(), "uuid subtype")
assert.Equal(t, 16, len(uuid.B), "UUID length")
actualRecorded.Remove("userId")

actualRecorded.Remove("credentials")

expectedRec := integration.ConvertDocument(t, bson.D{
{"_id", fmt.Sprintf("%s.%s", dbName, must.NotFail(payload.Get("createUser")))},
{"user", must.NotFail(payload.Get("createUser"))},
}) // FIXME
{"db", dbName},
{"roles", bson.A{}},
})

testutil.AssertEqual(t, expectedRec, actualRecorded)
// TODO compare other data
}
3 changes: 3 additions & 0 deletions internal/handler/handlererrors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,9 @@ const (
// by command-line or config file.
ErrFreeMonitoringDisabled = ErrorCode(50840) // Location50840

// ErrUserAlreadyExists indicates that user already exists.
ErrUserAlreadyExists = ErrorCode(51003) // Location51003

// ErrValueNegative indicates that value must not be negative.
ErrValueNegative = ErrorCode(51024) // Location51024

Expand Down
30 changes: 16 additions & 14 deletions internal/handler/handlererrors/errorcode_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 48 additions & 12 deletions internal/handler/msg_createuser.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@

import (
"context"
"errors"
"fmt"

"github.com/google/uuid"

"github.com/FerretDB/FerretDB/internal/backends"
"github.com/FerretDB/FerretDB/internal/handler/common"
"github.com/FerretDB/FerretDB/internal/handler/handlererrors"
"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/internal/util/must"
Expand All @@ -34,83 +37,116 @@
return nil, lazyerrors.Error(err)
}

dbName, err := common.GetRequiredParam[string](document, "$db")
if err != nil {
return nil, err
}

Check warning on line 43 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L40-L43

Added lines #L40 - L43 were not covered by tests

if dbName != "$external" && !document.Has("pwd") {
return nil, handlererrors.NewCommandErrorMsg(
handlererrors.ErrBadValue,
"Must provide a 'pwd' field for all user documents, except those with '$external' as the user's source db",
)
}

Check warning on line 50 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L45-L50

Added lines #L45 - L50 were not covered by tests

// https://www.mongodb.com/docs/manual/reference/command/createUser/

username, err := common.GetRequiredParam[string](document, document.Command())
var username string
username, err = common.GetRequiredParam[string](document, document.Command())

if err != nil {
return nil, err
}

Check warning on line 59 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L54-L59

Added lines #L54 - L59 were not covered by tests

if err := common.UnimplementedNonDefault(document, "customData", func(v any) bool {
if err = common.UnimplementedNonDefault(document, "customData", func(v any) bool {
if v == nil || v == types.Null {
return true
}

Check warning on line 64 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L61-L64

Added lines #L61 - L64 were not covered by tests

cd, ok := v.(*types.Document)
return ok && cd.Len() == 0
}); err != nil {
return nil, err
}

Check warning on line 70 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L66-L70

Added lines #L66 - L70 were not covered by tests

if err := common.UnimplementedNonDefault(document, "roles", func(v any) bool {
if _, err = common.GetRequiredParam[*types.Array](document, "roles"); err != nil {
var ce *handlererrors.CommandError
if errors.As(err, &ce) && ce.Code() == handlererrors.ErrBadValue {
return nil, handlererrors.NewCommandErrorMsg(
handlererrors.ErrMissingField,
"BSON field 'createUser.roles' is missing but a required field",
)
}

Check warning on line 79 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L72-L79

Added lines #L72 - L79 were not covered by tests

return nil, lazyerrors.Error(err)

Check warning on line 81 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L81

Added line #L81 was not covered by tests
}

if err = common.UnimplementedNonDefault(document, "roles", func(v any) bool {
if v == nil || v == types.Null {
return true
return false
}

Check warning on line 87 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L84-L87

Added lines #L84 - L87 were not covered by tests

roles, ok := v.(*types.Array)
return ok && roles.Len() == 0
r, ok := v.(*types.Array)
return ok && r.Len() == 0
}); err != nil {
return nil, err
}

Check warning on line 93 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L89-L93

Added lines #L89 - L93 were not covered by tests

if err := common.UnimplementedNonDefault(document, "digestPassword", func(v any) bool {
if err = common.UnimplementedNonDefault(document, "digestPassword", func(v any) bool {
if v == nil || v == types.Null {
return true
}

Check warning on line 98 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L95-L98

Added lines #L95 - L98 were not covered by tests

dp, ok := v.(bool)
return ok && dp
}); err != nil {
return nil, err
}

Check warning on line 104 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L100-L104

Added lines #L100 - L104 were not covered by tests

// NOTE: In MongoDB, the comment field isn't saved in the database, but used for log and profiling.
common.Ignored(document, h.L, "pwd", "writeConcern", "authenticationRestrictions", "mechanisms", "comment")

id := uuid.New()
saved := must.NotFail(types.NewDocument(
"_id", dbName+"."+username,
"userID", types.Binary{Subtype: types.BinaryUUID, B: []byte(uuid.NewString())},
"user", username,
"credentials", types.MakeDocument(0),
"user", username,
"db", dbName,
"roles", types.MakeArray(0),
"userId", types.Binary{Subtype: types.BinaryUUID, B: id[:]},
))

db, err := h.b.Database(dbName)
// Users are saved in the "admin" database.
adminDB, err := h.b.Database("admin")
if err != nil {
return nil, lazyerrors.Error(err)
}

Check warning on line 123 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L107-L123

Added lines #L107 - L123 were not covered by tests

collection, err := db.Collection("system.users")
users, err := adminDB.Collection("system.users")
if err != nil {
return nil, lazyerrors.Error(err)
}

Check warning on line 128 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L125-L128

Added lines #L125 - L128 were not covered by tests

_, err = collection.InsertAll(ctx, &backends.InsertAllParams{
_, err = users.InsertAll(ctx, &backends.InsertAllParams{
Docs: []*types.Document{saved},
})
if err != nil {
if backends.ErrorCodeIs(err, backends.ErrorCodeInsertDuplicateID) {
return nil, handlererrors.NewCommandErrorMsg(
handlererrors.ErrUserAlreadyExists,
fmt.Sprintf("User \"%s@%s\" already exists", username, dbName),
)
}

Check warning on line 139 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L130-L139

Added lines #L130 - L139 were not covered by tests

return nil, lazyerrors.Error(err)

Check warning on line 141 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L141

Added line #L141 was not covered by tests
}

var reply wire.OpMsg
must.NoError(reply.SetSections(wire.OpMsgSection{
Documents: []*types.Document{must.NotFail(types.NewDocument(
"ok", float64(1),
))},
}))

return &reply, nil

Check warning on line 151 in internal/handler/msg_createuser.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/msg_createuser.go#L144-L151

Added lines #L144 - L151 were not covered by tests
}
Loading