From 45c4844019e209c99a6dc61d349cd198aa2a310a Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Wed, 29 Nov 2023 12:30:13 +0400 Subject: [PATCH 1/8] Allow `system.` prefix for collections for now --- internal/backends/validation.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/backends/validation.go b/internal/backends/validation.go index 160eb353b190..f40c58b27342 100644 --- a/internal/backends/validation.go +++ b/internal/backends/validation.go @@ -55,7 +55,8 @@ func validateDatabaseName(name string) error { // // It follows MongoDB restrictions plus: // - allows only UTF-8 characters; -// - disallows '.' prefix (MongoDB fails to work with such collections correctly too); +// - allows `system.` prefix ("system" collections are just regular collections); +// - disallows `.` prefix (MongoDB fails to work with such collections correctly too); // - disallows `_ferretdb_` prefix. // // That validation is quite lax because @@ -67,7 +68,7 @@ func validateCollectionName(name string) error { return NewError(ErrorCodeCollectionNameIsInvalid, nil) } - if strings.HasPrefix(name, ReservedPrefix) || strings.HasPrefix(name, "system.") { + if strings.HasPrefix(name, ReservedPrefix) { return NewError(ErrorCodeCollectionNameIsInvalid, nil) } From 457fcace79f4547ce02b41e38c589de0f49bf2b2 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Wed, 29 Nov 2023 13:04:55 +0400 Subject: [PATCH 2/8] Add stubs for authentication commands --- cmd/ferretdb/main.go | 2 + integration/setup/listener.go | 1 + internal/clientconn/conn.go | 4 +- internal/handler/commands.go | 249 ++++++++++++++++++++++++ internal/handler/handler.go | 15 +- internal/handler/msg_createuser.go | 34 ++++ internal/handler/msg_dropuser.go | 34 ++++ internal/handler/msg_listcommands.go | 216 +------------------- internal/handler/registry/hana.go | 1 + internal/handler/registry/mysql.go | 1 + internal/handler/registry/postgresql.go | 1 + internal/handler/registry/registry.go | 1 + internal/handler/registry/sqlite.go | 1 + 13 files changed, 339 insertions(+), 221 deletions(-) create mode 100644 internal/handler/commands.go create mode 100644 internal/handler/msg_createuser.go create mode 100644 internal/handler/msg_dropuser.go diff --git a/cmd/ferretdb/main.go b/cmd/ferretdb/main.go index d9b281620410..245a497308a2 100644 --- a/cmd/ferretdb/main.go +++ b/cmd/ferretdb/main.go @@ -88,6 +88,7 @@ var cli struct { DisableFilterPushdown bool `default:"false" help:"Experimental: disable filter pushdown."` EnableOplog bool `default:"false" help:"Experimental: enable capped collections, tailable cursors and OpLog." hidden:""` + EnableNewAuth bool `default:"false" help:"Experimental: enable new authentication." hidden:""` //nolint:lll // for readability Telemetry struct { @@ -391,6 +392,7 @@ func run() { TestOpts: registry.TestOpts{ DisableFilterPushdown: cli.Test.DisableFilterPushdown, EnableOplog: cli.Test.EnableOplog, + EnableNewAuth: cli.Test.EnableNewAuth, }, }) if err != nil { diff --git a/integration/setup/listener.go b/integration/setup/listener.go index a31b59972392..549f41a2bb5e 100644 --- a/integration/setup/listener.go +++ b/integration/setup/listener.go @@ -161,6 +161,7 @@ func setupListener(tb testtb.TB, ctx context.Context, logger *zap.Logger) string TestOpts: registry.TestOpts{ DisableFilterPushdown: *disableFilterPushdownF, EnableOplog: true, + EnableNewAuth: false, }, } h, closeBackend, err := registry.NewHandler(handler, handlerOpts) diff --git a/internal/clientconn/conn.go b/internal/clientconn/conn.go index 6b427c9ce1b2..5a74ea64ca79 100644 --- a/internal/clientconn/conn.go +++ b/internal/clientconn/conn.go @@ -555,7 +555,7 @@ func (c *conn) route(ctx context.Context, reqHeader *wire.MsgHeader, reqBody wir // // The passed context is canceled when the client disconnects. func (c *conn) handleOpMsg(ctx context.Context, msg *wire.OpMsg, command string) (*wire.OpMsg, error) { - if cmd, ok := handler.Commands[command]; ok { + if cmd, ok := c.h.Commands()[command]; ok { if cmd.Handler != nil { defer observability.FuncCall(ctx)() @@ -563,7 +563,7 @@ func (c *conn) handleOpMsg(ctx context.Context, msg *wire.OpMsg, command string) ctx = pprof.WithLabels(ctx, pprof.Labels("command", command)) pprof.SetGoroutineLabels(ctx) - return cmd.Handler(c.h, ctx, msg) + return cmd.Handler(ctx, msg) } } diff --git a/internal/handler/commands.go b/internal/handler/commands.go new file mode 100644 index 000000000000..d0d4137bb7f8 --- /dev/null +++ b/internal/handler/commands.go @@ -0,0 +1,249 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/wire" +) + +// command represents a handler command. +type command struct { + // Handler processes this command. + // + // The passed context is canceled when the client disconnects. + Handler func(context.Context, *wire.OpMsg) (*wire.OpMsg, error) + + // Help is shown in the help function. + // If empty, that command is skipped in `listCommands` output. + Help string +} + +// FIXME Commands maps commands names to descriptions and implementations. +func (h *Handler) initCommands() { + h.commands = map[string]command{ + // sorted alphabetically + "aggregate": { + Handler: h.MsgAggregate, + Help: "Returns aggregated data.", + }, + "buildInfo": { + Handler: h.MsgBuildInfo, + Help: "Returns a summary of the build information.", + }, + "buildinfo": { // old lowercase variant + Handler: h.MsgBuildInfo, + Help: "", // hidden + }, + "collMod": { + Handler: h.MsgCollMod, + Help: "Adds options to a collection or modify view definitions.", + }, + "collStats": { + Handler: h.MsgCollStats, + Help: "Returns storage data for a collection.", + }, + "compact": { + Handler: h.MsgCompact, + Help: "Reduces the disk space collection takes and refreshes its statistics.", + }, + "connectionStatus": { + Handler: h.MsgConnectionStatus, + Help: "Returns information about the current connection, " + + "specifically the state of authenticated users and their available permissions.", + }, + "count": { + Handler: h.MsgCount, + Help: "Returns the count of documents that's matched by the query.", + }, + "create": { + Handler: h.MsgCreate, + Help: "Creates the collection.", + }, + "createIndexes": { + Handler: h.MsgCreateIndexes, + Help: "Creates indexes on a collection.", + }, + "currentOp": { + Handler: h.MsgCurrentOp, + Help: "Returns information about operations currently in progress.", + }, + "dataSize": { + Handler: h.MsgDataSize, + Help: "Returns the size of the collection in bytes.", + }, + "dbStats": { + Handler: h.MsgDBStats, + Help: "Returns the statistics of the database.", + }, + "dbstats": { // old lowercase variant + Handler: h.MsgDBStats, + Help: "", // hidden + }, + "debugError": { + Handler: h.MsgDebugError, + Help: "Returns error for debugging.", + }, + "delete": { + Handler: h.MsgDelete, + Help: "Deletes documents matched by the query.", + }, + "distinct": { + Handler: h.MsgDistinct, + Help: "Returns an array of distinct values for the given field.", + }, + "drop": { + Handler: h.MsgDrop, + Help: "Drops the collection.", + }, + "dropDatabase": { + Handler: h.MsgDropDatabase, + Help: "Drops production database.", + }, + "dropIndexes": { + Handler: h.MsgDropIndexes, + Help: "Drops indexes on a collection.", + }, + "explain": { + Handler: h.MsgExplain, + Help: "Returns the execution plan.", + }, + "find": { + Handler: h.MsgFind, + Help: "Returns documents matched by the query.", + }, + "findAndModify": { + Handler: h.MsgFindAndModify, + Help: "Docs, updates, or deletes, and returns a document matched by the query.", + }, + "findandmodify": { // old lowercase variant + Handler: h.MsgFindAndModify, + Help: "", // hidden + }, + "getCmdLineOpts": { + Handler: h.MsgGetCmdLineOpts, + Help: "Returns a summary of all runtime and configuration options.", + }, + "getFreeMonitoringStatus": { + Handler: h.MsgGetFreeMonitoringStatus, + Help: "Returns a status of the free monitoring.", + }, + "getLog": { + Handler: h.MsgGetLog, + Help: "Returns the most recent logged events from memory.", + }, + "getMore": { + Handler: h.MsgGetMore, + Help: "Returns the next batch of documents from a cursor.", + }, + "getParameter": { + Handler: h.MsgGetParameter, + Help: "Returns the value of the parameter.", + }, + "hello": { + Handler: h.MsgHello, + Help: "Returns the role of the FerretDB instance.", + }, + "hostInfo": { + Handler: h.MsgHostInfo, + Help: "Returns a summary of the system information.", + }, + "insert": { + Handler: h.MsgInsert, + Help: "Docs documents into the database.", + }, + "isMaster": { + Handler: h.MsgIsMaster, + Help: "Returns the role of the FerretDB instance.", + }, + "ismaster": { // old lowercase variant + Handler: h.MsgIsMaster, + Help: "", // hidden + }, + "killCursors": { + Handler: h.MsgKillCursors, + Help: "Closes server cursors.", + }, + "listCollections": { + Handler: h.MsgListCollections, + Help: "Returns the information of the collections and views in the database.", + }, + "listCommands": { + Handler: h.MsgListCommands, + Help: "Returns a list of currently supported commands.", + }, + "listDatabases": { + Handler: h.MsgListDatabases, + Help: "Returns a summary of all the databases.", + }, + "listIndexes": { + Handler: h.MsgListIndexes, + Help: "Returns a summary of indexes of the specified collection.", + }, + "logout": { + Handler: h.MsgLogout, + Help: "Logs out from the current session.", + }, + "ping": { + Handler: h.MsgPing, + Help: "Returns a pong response.", + }, + "renameCollection": { + Handler: h.MsgRenameCollection, + Help: "Changes the name of an existing collection.", + }, + "saslStart": { + Handler: h.MsgSASLStart, + Help: "Starts a SASL conversation.", + }, + "serverStatus": { + Handler: h.MsgServerStatus, + Help: "Returns an overview of the databases state.", + }, + "setFreeMonitoring": { + Handler: h.MsgSetFreeMonitoring, + Help: "Toggles free monitoring.", + }, + "update": { + Handler: h.MsgUpdate, + Help: "Updates documents that are matched by the query.", + }, + "validate": { + Handler: h.MsgValidate, + Help: "Validate collection.", + }, + "whatsmyuri": { + Handler: h.MsgWhatsMyURI, + Help: "Returns peer information.", + }, + // please keep sorted alphabetically + } + + if h.EnableNewAuth { + h.commands["createUser"] = command{ + Handler: h.MsgCreateUser, + Help: "Creates a new user.", + } + h.commands["dropUser"] = command{ + Handler: h.MsgDropUser, + Help: "Drops user.", + } + } +} + +func (h *Handler) Commands() map[string]command { + return h.commands +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 1a5037df0db4..6c5d8bc89654 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -31,14 +31,14 @@ import ( // MsgXXX methods handle OP_MSG commands. // CmdQuery handles a limited subset of OP_QUERY messages. // -// Handler is shared between all connections! Be careful when you need connection-specific information. -// Currently, we pass connection information through context, see `ConnInfo` and its usage. +// Handler instance is shared between all client connections. type Handler struct { *NewOpts b backends.Backend - cursors *cursor.Registry + cursors *cursor.Registry + commands map[string]command } // NewOpts represents handler configuration. @@ -54,6 +54,7 @@ type NewOpts struct { // test options DisableFilterPushdown bool EnableOplog bool + EnableNewAuth bool } // New returns a new handler. @@ -64,11 +65,15 @@ func New(opts *NewOpts) (*Handler, error) { b = oplog.NewBackend(b, opts.L.Named("oplog")) } - return &Handler{ + h := &Handler{ b: b, NewOpts: opts, cursors: cursor.NewRegistry(opts.L.Named("cursors")), - }, nil + } + + h.initCommands() + + return h, nil } // Close gracefully shutdowns handler. diff --git a/internal/handler/msg_createuser.go b/internal/handler/msg_createuser.go new file mode 100644 index 000000000000..5c83d098c608 --- /dev/null +++ b/internal/handler/msg_createuser.go @@ -0,0 +1,34 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/handler/common" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgCreateUser implements `createUser` command. +func (h *Handler) MsgCreateUser(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + // TODO https://github.com/FerretDB/FerretDB/issues/1491 + return nil, common.Unimplemented(document, document.Command()) +} diff --git a/internal/handler/msg_dropuser.go b/internal/handler/msg_dropuser.go new file mode 100644 index 000000000000..b915f518d2ac --- /dev/null +++ b/internal/handler/msg_dropuser.go @@ -0,0 +1,34 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/handler/common" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgDropUser implements `dropUser` command. +func (h *Handler) MsgDropUser(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + // TODO https://github.com/FerretDB/FerretDB/issues/1493 + return nil, common.Unimplemented(document, document.Command()) +} diff --git a/internal/handler/msg_listcommands.go b/internal/handler/msg_listcommands.go index 5cd9b136d824..87c6f0b411bf 100644 --- a/internal/handler/msg_listcommands.go +++ b/internal/handler/msg_listcommands.go @@ -25,226 +25,14 @@ import ( "github.com/FerretDB/FerretDB/internal/wire" ) -// command represents a handler command. -type command struct { - // Handler processes this command. - // - // The passed context is canceled when the client disconnects. - Handler func(*Handler, context.Context, *wire.OpMsg) (*wire.OpMsg, error) - - // Help is shown in the help function. - // If empty, that command is skipped in `listCommands` output. - Help string -} - -// Commands maps commands names to descriptions and implementations. -var Commands = map[string]command{ - // sorted alphabetically - "aggregate": { - Handler: (*Handler).MsgAggregate, - Help: "Returns aggregated data.", - }, - "buildInfo": { - Handler: (*Handler).MsgBuildInfo, - Help: "Returns a summary of the build information.", - }, - "buildinfo": { // old lowercase variant - Handler: (*Handler).MsgBuildInfo, - }, - "collMod": { - Handler: (*Handler).MsgCollMod, - Help: "Adds options to a collection or modify view definitions.", - }, - "collStats": { - Handler: (*Handler).MsgCollStats, - Help: "Returns storage data for a collection.", - }, - "compact": { - Handler: (*Handler).MsgCompact, - Help: "Reduces the disk space collection takes and refreshes its statistics.", - }, - "connectionStatus": { - Handler: (*Handler).MsgConnectionStatus, - Help: "Returns information about the current connection, " + - "specifically the state of authenticated users and their available permissions.", - }, - "count": { - Handler: (*Handler).MsgCount, - Help: "Returns the count of documents that's matched by the query.", - }, - "create": { - Handler: (*Handler).MsgCreate, - Help: "Creates the collection.", - }, - "createIndexes": { - Handler: (*Handler).MsgCreateIndexes, - Help: "Creates indexes on a collection.", - }, - "currentOp": { - Handler: (*Handler).MsgCurrentOp, - Help: "Returns information about operations currently in progress.", - }, - "dataSize": { - Handler: (*Handler).MsgDataSize, - Help: "Returns the size of the collection in bytes.", - }, - "dbStats": { - Handler: (*Handler).MsgDBStats, - Help: "Returns the statistics of the database.", - }, - "dbstats": { // old lowercase variant - Handler: (*Handler).MsgDBStats, - }, - "debugError": { - Handler: (*Handler).MsgDebugError, - Help: "Returns error for debugging.", - }, - "delete": { - Handler: (*Handler).MsgDelete, - Help: "Deletes documents matched by the query.", - }, - "distinct": { - Handler: (*Handler).MsgDistinct, - Help: "Returns an array of distinct values for the given field.", - }, - "drop": { - Handler: (*Handler).MsgDrop, - Help: "Drops the collection.", - }, - "dropDatabase": { - Handler: (*Handler).MsgDropDatabase, - Help: "Drops production database.", - }, - "dropIndexes": { - Handler: (*Handler).MsgDropIndexes, - Help: "Drops indexes on a collection.", - }, - "explain": { - Handler: (*Handler).MsgExplain, - Help: "Returns the execution plan.", - }, - "find": { - Handler: (*Handler).MsgFind, - Help: "Returns documents matched by the query.", - }, - "findAndModify": { - Handler: (*Handler).MsgFindAndModify, - Help: "Docs, updates, or deletes, and returns a document matched by the query.", - }, - "findandmodify": { // old lowercase variant - Handler: (*Handler).MsgFindAndModify, - }, - "getCmdLineOpts": { - Handler: (*Handler).MsgGetCmdLineOpts, - Help: "Returns a summary of all runtime and configuration options.", - }, - "getFreeMonitoringStatus": { - Handler: (*Handler).MsgGetFreeMonitoringStatus, - Help: "Returns a status of the free monitoring.", - }, - "getLog": { - Handler: (*Handler).MsgGetLog, - Help: "Returns the most recent logged events from memory.", - }, - "getMore": { - Handler: (*Handler).MsgGetMore, - Help: "Returns the next batch of documents from a cursor.", - }, - "getParameter": { - Handler: (*Handler).MsgGetParameter, - Help: "Returns the value of the parameter.", - }, - "hello": { - Handler: (*Handler).MsgHello, - Help: "Returns the role of the FerretDB instance.", - }, - "hostInfo": { - Handler: (*Handler).MsgHostInfo, - Help: "Returns a summary of the system information.", - }, - "insert": { - Handler: (*Handler).MsgInsert, - Help: "Docs documents into the database.", - }, - "isMaster": { - Handler: (*Handler).MsgIsMaster, - Help: "Returns the role of the FerretDB instance.", - }, - "ismaster": { // old lowercase variant - Handler: (*Handler).MsgIsMaster, - }, - "killCursors": { - Handler: (*Handler).MsgKillCursors, - Help: "Closes server cursors.", - }, - "listCollections": { - Handler: (*Handler).MsgListCollections, - Help: "Returns the information of the collections and views in the database.", - }, - // listCommands is added by the init() function below. - "listDatabases": { - Handler: (*Handler).MsgListDatabases, - Help: "Returns a summary of all the databases.", - }, - "listIndexes": { - Handler: (*Handler).MsgListIndexes, - Help: "Returns a summary of indexes of the specified collection.", - }, - "logout": { - Handler: (*Handler).MsgLogout, - Help: "Logs out from the current session.", - }, - "ping": { - Handler: (*Handler).MsgPing, - Help: "Returns a pong response.", - }, - "renameCollection": { - Handler: (*Handler).MsgRenameCollection, - Help: "Changes the name of an existing collection.", - }, - "saslStart": { - Handler: (*Handler).MsgSASLStart, - Help: "Starts a SASL conversation.", - }, - "serverStatus": { - Handler: (*Handler).MsgServerStatus, - Help: "Returns an overview of the databases state.", - }, - "setFreeMonitoring": { - Handler: (*Handler).MsgSetFreeMonitoring, - Help: "Toggles free monitoring.", - }, - "update": { - Handler: (*Handler).MsgUpdate, - Help: "Updates documents that are matched by the query.", - }, - "validate": { - Handler: (*Handler).MsgValidate, - Help: "Validate collection.", - }, - "whatsmyuri": { - Handler: (*Handler).MsgWhatsMyURI, - Help: "Returns peer information.", - }, - // please keep sorted alphabetically -} - -func init() { - // to prevent the initialization cycle - Commands["listCommands"] = command{ - Handler: (*Handler).MsgListCommands, - Help: "Returns a list of currently supported commands.", - } -} - // MsgListCommands implements `listCommands` command. func (h *Handler) MsgListCommands(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { cmdList := must.NotFail(types.NewDocument()) - names := maps.Keys(Commands) + names := maps.Keys(h.commands) sort.Strings(names) for _, name := range names { - cmd := Commands[name] + cmd := h.commands[name] if cmd.Help == "" { continue } diff --git a/internal/handler/registry/hana.go b/internal/handler/registry/hana.go index 318fd4df82ca..e4e5b429114f 100644 --- a/internal/handler/registry/hana.go +++ b/internal/handler/registry/hana.go @@ -44,6 +44,7 @@ func init() { DisableFilterPushdown: opts.DisableFilterPushdown, EnableOplog: opts.EnableOplog, + EnableNewAuth: opts.EnableNewAuth, } h, err := handler.New(handlerOpts) diff --git a/internal/handler/registry/mysql.go b/internal/handler/registry/mysql.go index a215fc365e27..16ed50155d85 100644 --- a/internal/handler/registry/mysql.go +++ b/internal/handler/registry/mysql.go @@ -40,6 +40,7 @@ func init() { DisableFilterPushdown: opts.DisableFilterPushdown, EnableOplog: opts.EnableOplog, + EnableNewAuth: opts.EnableNewAuth, } h, err := handler.New(handlerOpts) diff --git a/internal/handler/registry/postgresql.go b/internal/handler/registry/postgresql.go index 8337d532d81a..7c8dabdaf1b3 100644 --- a/internal/handler/registry/postgresql.go +++ b/internal/handler/registry/postgresql.go @@ -40,6 +40,7 @@ func init() { DisableFilterPushdown: opts.DisableFilterPushdown, EnableOplog: opts.EnableOplog, + EnableNewAuth: opts.EnableNewAuth, } h, err := handler.New(handlerOpts) diff --git a/internal/handler/registry/registry.go b/internal/handler/registry/registry.go index a44d5e9105e2..5a22729f07d5 100644 --- a/internal/handler/registry/registry.go +++ b/internal/handler/registry/registry.go @@ -63,6 +63,7 @@ type NewHandlerOpts struct { type TestOpts struct { DisableFilterPushdown bool EnableOplog bool + EnableNewAuth bool } // NewHandler constructs a new handler. diff --git a/internal/handler/registry/sqlite.go b/internal/handler/registry/sqlite.go index b1308c4aa612..803e6f79e762 100644 --- a/internal/handler/registry/sqlite.go +++ b/internal/handler/registry/sqlite.go @@ -40,6 +40,7 @@ func init() { DisableFilterPushdown: opts.DisableFilterPushdown, EnableOplog: opts.EnableOplog, + EnableNewAuth: opts.EnableNewAuth, } h, err := handler.New(handlerOpts) From a63e2b841e2cfe18881bf326abff1734c794eb93 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Wed, 29 Nov 2023 14:19:04 +0400 Subject: [PATCH 3/8] More stubs --- cmd/ferretdb/main.go | 2 +- internal/handler/commands.go | 17 +++++++++- .../handler/msg_dropallusersfromdatabase.go | 34 +++++++++++++++++++ internal/handler/msg_listcommands.go | 4 +-- internal/handler/msg_updateuser.go | 34 +++++++++++++++++++ internal/handler/msg_usersinfo.go | 34 +++++++++++++++++++ 6 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 internal/handler/msg_dropallusersfromdatabase.go create mode 100644 internal/handler/msg_updateuser.go create mode 100644 internal/handler/msg_usersinfo.go diff --git a/cmd/ferretdb/main.go b/cmd/ferretdb/main.go index 245a497308a2..f9478603fd95 100644 --- a/cmd/ferretdb/main.go +++ b/cmd/ferretdb/main.go @@ -88,7 +88,7 @@ var cli struct { DisableFilterPushdown bool `default:"false" help:"Experimental: disable filter pushdown."` EnableOplog bool `default:"false" help:"Experimental: enable capped collections, tailable cursors and OpLog." hidden:""` - EnableNewAuth bool `default:"false" help:"Experimental: enable new authentication." hidden:""` + EnableNewAuth bool `default:"false" help:"Experimental: enable new authentication." hidden:""` //nolint:lll // for readability Telemetry struct { diff --git a/internal/handler/commands.go b/internal/handler/commands.go index d0d4137bb7f8..fcabb36ebbaf 100644 --- a/internal/handler/commands.go +++ b/internal/handler/commands.go @@ -32,7 +32,7 @@ type command struct { Help string } -// FIXME Commands maps commands names to descriptions and implementations. +// initCommands initializes the commands map for that handler instance. func (h *Handler) initCommands() { h.commands = map[string]command{ // sorted alphabetically @@ -233,17 +233,32 @@ func (h *Handler) initCommands() { } if h.EnableNewAuth { + // sorted alphabetically h.commands["createUser"] = command{ Handler: h.MsgCreateUser, Help: "Creates a new user.", } + h.commands["dropAllUsersFromDatabase"] = command{ + Handler: h.MsgDropAllUsersFromDatabase, + Help: "Drops all user from database.", + } h.commands["dropUser"] = command{ Handler: h.MsgDropUser, Help: "Drops user.", } + h.commands["updateUser"] = command{ + Handler: h.MsgUpdateUser, + Help: "Updates user.", + } + h.commands["usersInfo"] = command{ + Handler: h.MsgUpdateUser, + Help: "Returns information about users.", + } + // please keep sorted alphabetically } } +// Commands returns a map of enabled commands. func (h *Handler) Commands() map[string]command { return h.commands } diff --git a/internal/handler/msg_dropallusersfromdatabase.go b/internal/handler/msg_dropallusersfromdatabase.go new file mode 100644 index 000000000000..ba64f2d5f27c --- /dev/null +++ b/internal/handler/msg_dropallusersfromdatabase.go @@ -0,0 +1,34 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/handler/common" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgDropAllUsersFromDatabase implements `dropAllUsersFromDatabase` command. +func (h *Handler) MsgDropAllUsersFromDatabase(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + // TODO https://github.com/FerretDB/FerretDB/issues/1492 + return nil, common.Unimplemented(document, document.Command()) +} diff --git a/internal/handler/msg_listcommands.go b/internal/handler/msg_listcommands.go index 87c6f0b411bf..d6b1155b275f 100644 --- a/internal/handler/msg_listcommands.go +++ b/internal/handler/msg_listcommands.go @@ -28,11 +28,11 @@ import ( // MsgListCommands implements `listCommands` command. func (h *Handler) MsgListCommands(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { cmdList := must.NotFail(types.NewDocument()) - names := maps.Keys(h.commands) + names := maps.Keys(h.Commands()) sort.Strings(names) for _, name := range names { - cmd := h.commands[name] + cmd := h.Commands()[name] if cmd.Help == "" { continue } diff --git a/internal/handler/msg_updateuser.go b/internal/handler/msg_updateuser.go new file mode 100644 index 000000000000..15396da0e207 --- /dev/null +++ b/internal/handler/msg_updateuser.go @@ -0,0 +1,34 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/handler/common" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgUpdateUser implements `updateUser` command. +func (h *Handler) MsgUpdateUser(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + // TODO https://github.com/FerretDB/FerretDB/issues/1496 + return nil, common.Unimplemented(document, document.Command()) +} diff --git a/internal/handler/msg_usersinfo.go b/internal/handler/msg_usersinfo.go new file mode 100644 index 000000000000..16b5d4473ba9 --- /dev/null +++ b/internal/handler/msg_usersinfo.go @@ -0,0 +1,34 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/handler/common" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgUsersInfo implements `usersInfo` command. +func (h *Handler) MsgUsersInfo(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + // TODO https://github.com/FerretDB/FerretDB/issues/1497 + return nil, common.Unimplemented(document, document.Command()) +} From 7b4b8424e3f4632def285abb914243546964f6b0 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Wed, 29 Nov 2023 14:26:33 +0400 Subject: [PATCH 4/8] Tweak comments --- internal/handler/commands.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/handler/commands.go b/internal/handler/commands.go index fcabb36ebbaf..896fb3c88b92 100644 --- a/internal/handler/commands.go +++ b/internal/handler/commands.go @@ -20,15 +20,15 @@ import ( "github.com/FerretDB/FerretDB/internal/wire" ) -// command represents a handler command. +// command represents a handler for single command. type command struct { // Handler processes this command. // // The passed context is canceled when the client disconnects. Handler func(context.Context, *wire.OpMsg) (*wire.OpMsg, error) - // Help is shown in the help function. - // If empty, that command is skipped in `listCommands` output. + // Help is shown in the `listCommands` command output. + // If empty, that command is hidden, but still can be used. Help string } @@ -127,7 +127,7 @@ func (h *Handler) initCommands() { }, "findAndModify": { Handler: h.MsgFindAndModify, - Help: "Docs, updates, or deletes, and returns a document matched by the query.", + Help: "Updates or deletes, and returns a document matched by the query.", }, "findandmodify": { // old lowercase variant Handler: h.MsgFindAndModify, @@ -163,7 +163,7 @@ func (h *Handler) initCommands() { }, "insert": { Handler: h.MsgInsert, - Help: "Docs documents into the database.", + Help: "Inserts documents into the database.", }, "isMaster": { Handler: h.MsgIsMaster, @@ -223,7 +223,7 @@ func (h *Handler) initCommands() { }, "validate": { Handler: h.MsgValidate, - Help: "Validate collection.", + Help: "Validates collection.", }, "whatsmyuri": { Handler: h.MsgWhatsMyURI, From 8d172cbbf37e750dd5258be891a0899ee1d12f9c Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Wed, 29 Nov 2023 16:14:58 +0400 Subject: [PATCH 5/8] Add TLS proxy --- Taskfile.yml | 20 ++++++++++ cmd/ferretdb/main.go | 25 ++++++++---- ferretdb/ferretdb.go | 5 ++- internal/clientconn/conn.go | 19 +++++---- internal/clientconn/listener.go | 67 +++++++++++--------------------- internal/handler/proxy/proxy.go | 36 +++++++++++++++-- internal/util/tlsutil/tlsutil.go | 66 +++++++++++++++++++++++++++++++ 7 files changed, 174 insertions(+), 64 deletions(-) create mode 100644 internal/util/tlsutil/tlsutil.go diff --git a/Taskfile.yml b/Taskfile.yml index 96c642168f3e..03d67c41da88 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -429,6 +429,26 @@ tasks: --test-records-dir=tmp/records --test-enable-oplog + run-proxy-secured: + desc: "Run FerretDB in diff-proxy mode (TLS, auth required)" + deps: [build-host] + cmds: + - > + bin/ferretdb{{exeExt}} + --listen-addr='' + --listen-tls=:27018 + --listen-tls-cert-file=./build/certs/server-cert.pem + --listen-tls-key-file=./build/certs/server-key.pem + --listen-tls-ca-file=./build/certs/rootCA-cert.pem + --proxy-addr=127.0.0.1:47018 + --proxy-tls-cert-file=./build/certs/client-cert.pem + --proxy-tls-key-file=./build/certs/client-key.pem + --proxy-tls-ca-file=./build/certs/rootCA-cert.pem + --mode=diff-proxy + --handler=pg + --postgresql-url='postgres://username@127.0.0.1:5433/ferretdb?search_path=' + --test-records-dir=tmp/records + lint: desc: "Run linters" cmds: diff --git a/cmd/ferretdb/main.go b/cmd/ferretdb/main.go index f9478603fd95..4f319cc26b27 100644 --- a/cmd/ferretdb/main.go +++ b/cmd/ferretdb/main.go @@ -64,11 +64,17 @@ var cli struct { TLS string `default:"" help:"Listen TLS address."` TLSCertFile string `default:"" help:"TLS cert file path."` TLSKeyFile string `default:"" help:"TLS key file path."` - TLSCAFile string `default:"" help:"TLS CA file path." name:"tls-ca-file"` + TLSCaFile string `default:"" help:"TLS CA file path."` } `embed:"" prefix:"listen-"` - ProxyAddr string `default:"" help:"Proxy address."` - DebugAddr string `default:"127.0.0.1:8088" help:"Listen address for HTTP handlers for metrics, pprof, etc."` + Proxy struct { + Addr string `default:"" help:"Proxy address."` + TLSCertFile string `default:"" help:"Proxy TLS cert file path."` + TLSKeyFile string `default:"" help:"Proxy TLS key file path."` + TLSCaFile string `default:"" help:"Proxy TLS CA file path."` + } `embed:"" prefix:"proxy-"` + + DebugAddr string `default:"127.0.0.1:8088" help:"Listen address for HTTP handlers for metrics, pprof, etc."` // see setCLIPlugins kong.Plugins @@ -402,14 +408,19 @@ func run() { defer closeBackend() l := clientconn.NewListener(&clientconn.NewListenerOpts{ - TCP: cli.Listen.Addr, - Unix: cli.Listen.Unix, + TCP: cli.Listen.Addr, + Unix: cli.Listen.Unix, + TLS: cli.Listen.TLS, TLSCertFile: cli.Listen.TLSCertFile, TLSKeyFile: cli.Listen.TLSKeyFile, - TLSCAFile: cli.Listen.TLSCAFile, + TLSCAFile: cli.Listen.TLSCaFile, + + ProxyAddr: cli.Proxy.Addr, + ProxyTLSCertFile: cli.Proxy.TLSCertFile, + ProxyTLSKeyFile: cli.Proxy.TLSKeyFile, + ProxyTLSCAFile: cli.Proxy.TLSCaFile, - ProxyAddr: cli.ProxyAddr, Mode: clientconn.Mode(cli.Mode), Metrics: metrics, Handler: h, diff --git a/ferretdb/ferretdb.go b/ferretdb/ferretdb.go index 0db0f3ae8eac..c210b87636d1 100644 --- a/ferretdb/ferretdb.go +++ b/ferretdb/ferretdb.go @@ -127,8 +127,9 @@ func New(config *Config) (*FerretDB, error) { } l := clientconn.NewListener(&clientconn.NewListenerOpts{ - TCP: config.Listener.TCP, - Unix: config.Listener.Unix, + TCP: config.Listener.TCP, + Unix: config.Listener.Unix, + TLS: config.Listener.TLS, TLSCertFile: config.Listener.TLSCertFile, TLSKeyFile: config.Listener.TLSKeyFile, diff --git a/internal/clientconn/conn.go b/internal/clientconn/conn.go index 5a74ea64ca79..0542ca8b70bc 100644 --- a/internal/clientconn/conn.go +++ b/internal/clientconn/conn.go @@ -84,12 +84,17 @@ type conn struct { // newConnOpts represents newConn options. type newConnOpts struct { - netConn net.Conn - mode Mode - l *zap.Logger - handler *handler.Handler - connMetrics *connmetrics.ConnMetrics - proxyAddr string + netConn net.Conn + mode Mode + l *zap.Logger + handler *handler.Handler + connMetrics *connmetrics.ConnMetrics + + proxyAddr string + proxyTLSCertFile string + proxyTLSKeyFile string + proxyTLSCAFile string + testRecordsDir string // if empty, no records are created } @@ -105,7 +110,7 @@ func newConn(opts *newConnOpts) (*conn, error) { var p *proxy.Router if opts.mode != NormalMode { var err error - if p, err = proxy.New(opts.proxyAddr); err != nil { + if p, err = proxy.New(opts.proxyAddr, opts.proxyTLSCertFile, opts.proxyTLSKeyFile, opts.proxyTLSCAFile); err != nil { return nil, lazyerrors.Error(err) } } diff --git a/internal/clientconn/listener.go b/internal/clientconn/listener.go index 0ddefbe3485f..fe9721cc9310 100644 --- a/internal/clientconn/listener.go +++ b/internal/clientconn/listener.go @@ -17,12 +17,10 @@ package clientconn import ( "context" "crypto/tls" - "crypto/x509" "errors" "fmt" "math/rand" "net" - "os" "runtime/pprof" "sync" "time" @@ -34,6 +32,7 @@ import ( "github.com/FerretDB/FerretDB/internal/handler" "github.com/FerretDB/FerretDB/internal/util/ctxutil" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/tlsutil" "github.com/FerretDB/FerretDB/internal/wire" ) @@ -53,14 +52,19 @@ type Listener struct { // NewListenerOpts represents listener configuration. type NewListenerOpts struct { - TCP string - Unix string + TCP string + Unix string + TLS string TLSCertFile string TLSKeyFile string TLSCAFile string - ProxyAddr string + ProxyAddr string + ProxyTLSCertFile string + ProxyTLSKeyFile string + ProxyTLSCAFile string + Mode Mode Metrics *connmetrics.ListenerMetrics Handler *handler.Handler @@ -201,44 +205,12 @@ type setupTLSListenerOpts struct { // setupTLSListener returns a new TLS listener or and error. func setupTLSListener(opts *setupTLSListenerOpts) (net.Listener, error) { - if _, err := os.Stat(opts.certFile); err != nil { - return nil, fmt.Errorf("TLS certificate file: %w", err) - } - - if _, err := os.Stat(opts.keyFile); err != nil { - return nil, fmt.Errorf("TLS key file: %w", err) - } - - cert, err := tls.LoadX509KeyPair(opts.certFile, opts.keyFile) + config, err := tlsutil.Config(opts.certFile, opts.keyFile, opts.caFile) if err != nil { return nil, err } - config := tls.Config{ - Certificates: []tls.Certificate{cert}, - } - - if opts.caFile != "" { - if _, err = os.Stat(opts.caFile); err != nil { - return nil, fmt.Errorf("TLS CA file: %w", err) - } - - var rootCA []byte - - if rootCA, err = os.ReadFile(opts.caFile); err != nil { - return nil, err - } - - roots := x509.NewCertPool() - if ok := roots.AppendCertsFromPEM(rootCA); !ok { - return nil, fmt.Errorf("failed to parse root certificate") - } - - config.ClientAuth = tls.RequireAndVerifyClientCert - config.ClientCAs = roots - } - - listener, err := tls.Listen("tcp", opts.addr, &config) + listener, err := tls.Listen("tcp", opts.addr, config) if err != nil { return nil, lazyerrors.Error(err) } @@ -302,12 +274,17 @@ func acceptLoop(ctx context.Context, listener net.Listener, wg *sync.WaitGroup, pprof.SetGoroutineLabels(runCtx) opts := &newConnOpts{ - netConn: netConn, - mode: l.Mode, - l: l.Logger.Named("// " + connID + " "), // derive from the original unnamed logger - handler: l.Handler, - connMetrics: l.Metrics.ConnMetrics, // share between all conns - proxyAddr: l.ProxyAddr, + netConn: netConn, + mode: l.Mode, + l: l.Logger.Named("// " + connID + " "), // derive from the original unnamed logger + handler: l.Handler, + connMetrics: l.Metrics.ConnMetrics, // share between all conns + + proxyAddr: l.ProxyAddr, + proxyTLSCertFile: l.ProxyTLSCertFile, + proxyTLSKeyFile: l.ProxyTLSKeyFile, + proxyTLSCAFile: l.ProxyTLSCAFile, + testRecordsDir: l.TestRecordsDir, } diff --git a/internal/handler/proxy/proxy.go b/internal/handler/proxy/proxy.go index da7add1a9482..dbde60f3e77b 100644 --- a/internal/handler/proxy/proxy.go +++ b/internal/handler/proxy/proxy.go @@ -18,8 +18,11 @@ package proxy import ( "bufio" "context" + "crypto/tls" "net" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/tlsutil" "github.com/FerretDB/FerretDB/internal/wire" ) @@ -31,10 +34,18 @@ type Router struct { } // New creates a new Router for a service with given address. -func New(addr string) (*Router, error) { - conn, err := net.Dial("tcp", addr) +func New(addr, certFile, keyFile, caFile string) (*Router, error) { + var conn net.Conn + var err error + + if certFile != "" { + conn, err = dialTLS(addr, certFile, keyFile, caFile) + } else { + conn, err = net.Dial("tcp", addr) + } + if err != nil { - return nil, err + return nil, lazyerrors.Error(err) } return &Router{ @@ -44,6 +55,25 @@ func New(addr string) (*Router, error) { }, nil } +// dialTLS connects to the given address using TLS. +func dialTLS(addr, certFile, keyFile, caFile string) (net.Conn, error) { + config, err := tlsutil.Config(certFile, keyFile, caFile) + if err != nil { + return nil, err + } + + conn, err := tls.Dial("tcp", addr, config) + if err != nil { + return nil, lazyerrors.Error(err) + } + + if err = conn.Handshake(); err != nil { + return nil, lazyerrors.Error(err) + } + + return conn, nil +} + // Close stops the handler. func (r *Router) Close() { r.conn.Close() diff --git a/internal/util/tlsutil/tlsutil.go b/internal/util/tlsutil/tlsutil.go new file mode 100644 index 000000000000..47075bcde098 --- /dev/null +++ b/internal/util/tlsutil/tlsutil.go @@ -0,0 +1,66 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tlsutil provides TLS utilities. +package tlsutil + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "os" +) + +// Config provides TLS configuration for the given certificate and key files. +// If CA file is provided, full authentication is enabled. +func Config(certFile, keyFile, caFile string) (*tls.Config, error) { + if _, err := os.Stat(certFile); err != nil { + return nil, fmt.Errorf("TLS certificate file: %w", err) + } + + if _, err := os.Stat(keyFile); err != nil { + return nil, fmt.Errorf("TLS key file: %w", err) + } + + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, fmt.Errorf("TLS file pair: %w", err) + } + + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + + if caFile != "" { + if _, err := os.Stat(caFile); err != nil { + return nil, fmt.Errorf("TLS CA file: %w", err) + } + + b, err := os.ReadFile(caFile) + if err != nil { + return nil, err + } + + ca := x509.NewCertPool() + if ok := ca.AppendCertsFromPEM(b); !ok { + return nil, fmt.Errorf("TLS CA file: failed to parse") + } + + config.ClientAuth = tls.RequireAndVerifyClientCert + config.ClientCAs = ca + config.RootCAs = ca + } + + return config, nil +} From e1dfd1ae53426ff9c13b4bf1daca4e776c930f20 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Wed, 29 Nov 2023 17:33:47 +0400 Subject: [PATCH 6/8] Add label --- .github/settings.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/settings.yml b/.github/settings.yml index ef45fc940c0b..20588351ff14 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -20,6 +20,9 @@ labels: - name: area/aggregations color: "#5319E7" description: Issues about aggregation pipelines + - name: area/auth + color: "#E4012C" + description: Issues about authentication - name: area/build color: "#52D58A" description: Issues about builds and CI From 4098330790b1b680cdac7ff6b6e3acd32d08d8a2 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 30 Nov 2023 08:47:46 +0400 Subject: [PATCH 7/8] Add TLS support to proxy mode --- Taskfile.yml | 20 ++++++++++ cmd/ferretdb/main.go | 25 ++++++++---- ferretdb/ferretdb.go | 5 ++- internal/clientconn/conn.go | 19 +++++---- internal/clientconn/listener.go | 67 +++++++++++--------------------- internal/handler/proxy/proxy.go | 36 +++++++++++++++-- internal/util/tlsutil/tlsutil.go | 66 +++++++++++++++++++++++++++++++ 7 files changed, 174 insertions(+), 64 deletions(-) create mode 100644 internal/util/tlsutil/tlsutil.go diff --git a/Taskfile.yml b/Taskfile.yml index 980c9627432d..c7b11cc32882 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -454,6 +454,26 @@ tasks: --test-records-dir=tmp/records --test-enable-oplog + run-proxy-secured: + desc: "Run FerretDB in diff-proxy mode (TLS, auth required)" + deps: [build-host] + cmds: + - > + bin/ferretdb{{exeExt}} + --listen-addr='' + --listen-tls=:27018 + --listen-tls-cert-file=./build/certs/server-cert.pem + --listen-tls-key-file=./build/certs/server-key.pem + --listen-tls-ca-file=./build/certs/rootCA-cert.pem + --proxy-addr=127.0.0.1:47018 + --proxy-tls-cert-file=./build/certs/client-cert.pem + --proxy-tls-key-file=./build/certs/client-key.pem + --proxy-tls-ca-file=./build/certs/rootCA-cert.pem + --mode=diff-proxy + --handler=pg + --postgresql-url='postgres://username@127.0.0.1:5433/ferretdb?search_path=' + --test-records-dir=tmp/records + lint: desc: "Run linters" cmds: diff --git a/cmd/ferretdb/main.go b/cmd/ferretdb/main.go index 32c033738be1..685d296d56db 100644 --- a/cmd/ferretdb/main.go +++ b/cmd/ferretdb/main.go @@ -64,11 +64,17 @@ var cli struct { TLS string `default:"" help:"Listen TLS address."` TLSCertFile string `default:"" help:"TLS cert file path."` TLSKeyFile string `default:"" help:"TLS key file path."` - TLSCAFile string `default:"" help:"TLS CA file path." name:"tls-ca-file"` + TLSCaFile string `default:"" help:"TLS CA file path."` } `embed:"" prefix:"listen-"` - ProxyAddr string `default:"" help:"Proxy address."` - DebugAddr string `default:"127.0.0.1:8088" help:"Listen address for HTTP handlers for metrics, pprof, etc."` + Proxy struct { + Addr string `default:"" help:"Proxy address."` + TLSCertFile string `default:"" help:"Proxy TLS cert file path."` + TLSKeyFile string `default:"" help:"Proxy TLS key file path."` + TLSCaFile string `default:"" help:"Proxy TLS CA file path."` + } `embed:"" prefix:"proxy-"` + + DebugAddr string `default:"127.0.0.1:8088" help:"Listen address for HTTP handlers for metrics, pprof, etc."` // see setCLIPlugins kong.Plugins @@ -400,14 +406,19 @@ func run() { defer closeBackend() l := clientconn.NewListener(&clientconn.NewListenerOpts{ - TCP: cli.Listen.Addr, - Unix: cli.Listen.Unix, + TCP: cli.Listen.Addr, + Unix: cli.Listen.Unix, + TLS: cli.Listen.TLS, TLSCertFile: cli.Listen.TLSCertFile, TLSKeyFile: cli.Listen.TLSKeyFile, - TLSCAFile: cli.Listen.TLSCAFile, + TLSCAFile: cli.Listen.TLSCaFile, + + ProxyAddr: cli.Proxy.Addr, + ProxyTLSCertFile: cli.Proxy.TLSCertFile, + ProxyTLSKeyFile: cli.Proxy.TLSKeyFile, + ProxyTLSCAFile: cli.Proxy.TLSCaFile, - ProxyAddr: cli.ProxyAddr, Mode: clientconn.Mode(cli.Mode), Metrics: metrics, Handler: h, diff --git a/ferretdb/ferretdb.go b/ferretdb/ferretdb.go index 0db0f3ae8eac..c210b87636d1 100644 --- a/ferretdb/ferretdb.go +++ b/ferretdb/ferretdb.go @@ -127,8 +127,9 @@ func New(config *Config) (*FerretDB, error) { } l := clientconn.NewListener(&clientconn.NewListenerOpts{ - TCP: config.Listener.TCP, - Unix: config.Listener.Unix, + TCP: config.Listener.TCP, + Unix: config.Listener.Unix, + TLS: config.Listener.TLS, TLSCertFile: config.Listener.TLSCertFile, TLSKeyFile: config.Listener.TLSKeyFile, diff --git a/internal/clientconn/conn.go b/internal/clientconn/conn.go index 6b427c9ce1b2..1ab149b85901 100644 --- a/internal/clientconn/conn.go +++ b/internal/clientconn/conn.go @@ -84,12 +84,17 @@ type conn struct { // newConnOpts represents newConn options. type newConnOpts struct { - netConn net.Conn - mode Mode - l *zap.Logger - handler *handler.Handler - connMetrics *connmetrics.ConnMetrics - proxyAddr string + netConn net.Conn + mode Mode + l *zap.Logger + handler *handler.Handler + connMetrics *connmetrics.ConnMetrics + + proxyAddr string + proxyTLSCertFile string + proxyTLSKeyFile string + proxyTLSCAFile string + testRecordsDir string // if empty, no records are created } @@ -105,7 +110,7 @@ func newConn(opts *newConnOpts) (*conn, error) { var p *proxy.Router if opts.mode != NormalMode { var err error - if p, err = proxy.New(opts.proxyAddr); err != nil { + if p, err = proxy.New(opts.proxyAddr, opts.proxyTLSCertFile, opts.proxyTLSKeyFile, opts.proxyTLSCAFile); err != nil { return nil, lazyerrors.Error(err) } } diff --git a/internal/clientconn/listener.go b/internal/clientconn/listener.go index 0ddefbe3485f..fe9721cc9310 100644 --- a/internal/clientconn/listener.go +++ b/internal/clientconn/listener.go @@ -17,12 +17,10 @@ package clientconn import ( "context" "crypto/tls" - "crypto/x509" "errors" "fmt" "math/rand" "net" - "os" "runtime/pprof" "sync" "time" @@ -34,6 +32,7 @@ import ( "github.com/FerretDB/FerretDB/internal/handler" "github.com/FerretDB/FerretDB/internal/util/ctxutil" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/tlsutil" "github.com/FerretDB/FerretDB/internal/wire" ) @@ -53,14 +52,19 @@ type Listener struct { // NewListenerOpts represents listener configuration. type NewListenerOpts struct { - TCP string - Unix string + TCP string + Unix string + TLS string TLSCertFile string TLSKeyFile string TLSCAFile string - ProxyAddr string + ProxyAddr string + ProxyTLSCertFile string + ProxyTLSKeyFile string + ProxyTLSCAFile string + Mode Mode Metrics *connmetrics.ListenerMetrics Handler *handler.Handler @@ -201,44 +205,12 @@ type setupTLSListenerOpts struct { // setupTLSListener returns a new TLS listener or and error. func setupTLSListener(opts *setupTLSListenerOpts) (net.Listener, error) { - if _, err := os.Stat(opts.certFile); err != nil { - return nil, fmt.Errorf("TLS certificate file: %w", err) - } - - if _, err := os.Stat(opts.keyFile); err != nil { - return nil, fmt.Errorf("TLS key file: %w", err) - } - - cert, err := tls.LoadX509KeyPair(opts.certFile, opts.keyFile) + config, err := tlsutil.Config(opts.certFile, opts.keyFile, opts.caFile) if err != nil { return nil, err } - config := tls.Config{ - Certificates: []tls.Certificate{cert}, - } - - if opts.caFile != "" { - if _, err = os.Stat(opts.caFile); err != nil { - return nil, fmt.Errorf("TLS CA file: %w", err) - } - - var rootCA []byte - - if rootCA, err = os.ReadFile(opts.caFile); err != nil { - return nil, err - } - - roots := x509.NewCertPool() - if ok := roots.AppendCertsFromPEM(rootCA); !ok { - return nil, fmt.Errorf("failed to parse root certificate") - } - - config.ClientAuth = tls.RequireAndVerifyClientCert - config.ClientCAs = roots - } - - listener, err := tls.Listen("tcp", opts.addr, &config) + listener, err := tls.Listen("tcp", opts.addr, config) if err != nil { return nil, lazyerrors.Error(err) } @@ -302,12 +274,17 @@ func acceptLoop(ctx context.Context, listener net.Listener, wg *sync.WaitGroup, pprof.SetGoroutineLabels(runCtx) opts := &newConnOpts{ - netConn: netConn, - mode: l.Mode, - l: l.Logger.Named("// " + connID + " "), // derive from the original unnamed logger - handler: l.Handler, - connMetrics: l.Metrics.ConnMetrics, // share between all conns - proxyAddr: l.ProxyAddr, + netConn: netConn, + mode: l.Mode, + l: l.Logger.Named("// " + connID + " "), // derive from the original unnamed logger + handler: l.Handler, + connMetrics: l.Metrics.ConnMetrics, // share between all conns + + proxyAddr: l.ProxyAddr, + proxyTLSCertFile: l.ProxyTLSCertFile, + proxyTLSKeyFile: l.ProxyTLSKeyFile, + proxyTLSCAFile: l.ProxyTLSCAFile, + testRecordsDir: l.TestRecordsDir, } diff --git a/internal/handler/proxy/proxy.go b/internal/handler/proxy/proxy.go index da7add1a9482..dbde60f3e77b 100644 --- a/internal/handler/proxy/proxy.go +++ b/internal/handler/proxy/proxy.go @@ -18,8 +18,11 @@ package proxy import ( "bufio" "context" + "crypto/tls" "net" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/tlsutil" "github.com/FerretDB/FerretDB/internal/wire" ) @@ -31,10 +34,18 @@ type Router struct { } // New creates a new Router for a service with given address. -func New(addr string) (*Router, error) { - conn, err := net.Dial("tcp", addr) +func New(addr, certFile, keyFile, caFile string) (*Router, error) { + var conn net.Conn + var err error + + if certFile != "" { + conn, err = dialTLS(addr, certFile, keyFile, caFile) + } else { + conn, err = net.Dial("tcp", addr) + } + if err != nil { - return nil, err + return nil, lazyerrors.Error(err) } return &Router{ @@ -44,6 +55,25 @@ func New(addr string) (*Router, error) { }, nil } +// dialTLS connects to the given address using TLS. +func dialTLS(addr, certFile, keyFile, caFile string) (net.Conn, error) { + config, err := tlsutil.Config(certFile, keyFile, caFile) + if err != nil { + return nil, err + } + + conn, err := tls.Dial("tcp", addr, config) + if err != nil { + return nil, lazyerrors.Error(err) + } + + if err = conn.Handshake(); err != nil { + return nil, lazyerrors.Error(err) + } + + return conn, nil +} + // Close stops the handler. func (r *Router) Close() { r.conn.Close() diff --git a/internal/util/tlsutil/tlsutil.go b/internal/util/tlsutil/tlsutil.go new file mode 100644 index 000000000000..47075bcde098 --- /dev/null +++ b/internal/util/tlsutil/tlsutil.go @@ -0,0 +1,66 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tlsutil provides TLS utilities. +package tlsutil + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "os" +) + +// Config provides TLS configuration for the given certificate and key files. +// If CA file is provided, full authentication is enabled. +func Config(certFile, keyFile, caFile string) (*tls.Config, error) { + if _, err := os.Stat(certFile); err != nil { + return nil, fmt.Errorf("TLS certificate file: %w", err) + } + + if _, err := os.Stat(keyFile); err != nil { + return nil, fmt.Errorf("TLS key file: %w", err) + } + + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, fmt.Errorf("TLS file pair: %w", err) + } + + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + + if caFile != "" { + if _, err := os.Stat(caFile); err != nil { + return nil, fmt.Errorf("TLS CA file: %w", err) + } + + b, err := os.ReadFile(caFile) + if err != nil { + return nil, err + } + + ca := x509.NewCertPool() + if ok := ca.AppendCertsFromPEM(b); !ok { + return nil, fmt.Errorf("TLS CA file: failed to parse") + } + + config.ClientAuth = tls.RequireAndVerifyClientCert + config.ClientCAs = ca + config.RootCAs = ca + } + + return config, nil +} From 5881ea3cf553f2fc1875a46607ac6fd8872eeda7 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 30 Nov 2023 09:01:45 +0400 Subject: [PATCH 8/8] Update docs --- website/docs/configuration/flags.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/docs/configuration/flags.md b/website/docs/configuration/flags.md index 4a840d3dfea9..44a1d895e1c9 100644 --- a/website/docs/configuration/flags.md +++ b/website/docs/configuration/flags.md @@ -40,6 +40,9 @@ Some default values are overridden in [our Docker image](../quickstart-guide/doc | `--listen-tls-key-file` | TLS key file path | `FERRETDB_LISTEN_TLS_KEY_FILE` | | | `--listen-tls-ca-file` | TLS CA file path | `FERRETDB_LISTEN_TLS_CA_FILE` | | | `--proxy-addr` | Proxy address | `FERRETDB_PROXY_ADDR` | | +| `--proxy-tls-cert-file` | Proxy TLS cert file path | `FERRETDB_PROXY_TLS_CERT_FILE` | | +| `--proxy-tls-key-file` | Proxy TLS key file path | `FERRETDB_PROXY_TLS_KEY_FILE` | | +| `--proxy-tls-ca-file` | Proxy TLS CA file path | `FERRETDB_PROXY_TLS_CA_FILE` | | | `--debug-addr` | Listen address for HTTP handlers for metrics, pprof, etc
(set to `-` to disable) | `FERRETDB_DEBUG_ADDR` | `127.0.0.1:8088`
(`:8088` for Docker) | ## Backend handlers