Skip to content

Commit

Permalink
Merge branch 'fulghum/persist_root_superuser' into fulghum-41a09942
Browse files Browse the repository at this point in the history
  • Loading branch information
fulghum committed Jan 15, 2025
2 parents d3bf58d + c36bd1c commit 427343e
Show file tree
Hide file tree
Showing 21 changed files with 372 additions and 154 deletions.
15 changes: 15 additions & 0 deletions docker/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,27 @@ _main() {
set -- "$@" --config=$CONFIG_PROVIDED
fi

# If DOLT_ROOT_HOST has been specified – create a root user for that host with the specified password
if [ -n "$DOLT_ROOT_HOST" ] && [ "$DOLT_ROOT_HOST" != 'localhost' ]; then
echo "Ensuring root@${DOLT_ROOT_HOST} superuser exists (DOLT_ROOT_HOST was specified)"
dolt sql -q "CREATE USER IF NOT EXISTS 'root'@'${DOLT_ROOT_HOST}' IDENTIFIED BY '${DOLT_ROOT_PASSWORD}';
ALTER USER 'root'@'${DOLT_ROOT_HOST}' IDENTIFIED BY '${DOLT_ROOT_PASSWORD}';
GRANT ALL ON *.* TO 'root'@'${DOLT_ROOT_HOST}' WITH GRANT OPTION;"
fi

# Ensure the root@localhost user exists, with the requested password
echo "Ensuring root@localhost user exists"
dolt sql -q "CREATE USER IF NOT EXISTS 'root'@'localhost' IDENTIFIED BY '${DOLT_ROOT_PASSWORD}';
ALTER USER 'root'@'localhost' IDENTIFIED BY '${DOLT_ROOT_PASSWORD}';
GRANT ALL ON *.* TO 'root'@'localhost' WITH GRANT OPTION;"

if [[ ! -f $INIT_COMPLETED ]]; then
# run any file provided in /docker-entrypoint-initdb.d directory before the server starts
docker_process_init_files /docker-entrypoint-initdb.d/*
touch $INIT_COMPLETED
fi

# switch this process over to executing dolt sql-server
exec dolt sql-server --host=0.0.0.0 --port=3306 "$@"
}

Expand Down
37 changes: 18 additions & 19 deletions docker/serverREADME.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,45 +76,44 @@ Learn more about Dolt use cases, configuration and guides to use dolt on our [do

# How to use this image

This image is for Dolt SQL Server, which is similar to MySQL Docker Image. Running this image without any arguments
is equivalent to running `dolt sql-server --host 0.0.0.0 --port 3306` command locally. The reason for persisted host
and port is that it allows user to connect to the server inside the container from the local host system through
port-mapping.
This image is for Dolt SQL Server, which is similar to the MySQL Docker image. Running this image without any arguments
is equivalent to running `dolt sql-server --host 0.0.0.0 --port 3306` command inside a Docker container.

To check out supported options for `dolt sql-server`, you can run the image with `--help` flag.
To see all supported options for `dolt sql-server`, you can run the image with `--help` flag.

```shell
$ docker run dolthub/dolt-sql-server:latest --help
```

### Connect to the server in the container from the host system

To be able to connect to the server running in the container, we need to set up a port to connect to locally that
maps to the port in the container. The host is set to `0.0.0.0` for accepting connections to any available network
interface.
From the host system, to connect to a server running in a container, we need to map a port on the host system to the port our sql-server is running on in the container.

```shell
$ docker run -p 3307:3306 dolthub/dolt-sql-server:latest
We also need a user account that has permission to connect to the server
from the host system's address. By default, as of Dolt version 1.46.0, the `root` superuser is configured to only allow connections from localhost. This is a security feature to prevent unauthorized access to the server. If you don't want to log in to the container and then connect to your sql-server, you can use the `DOLT_ROOT_HOST` and `DOLT_ROOT_PASSWORD` environment variables to control how the `root` superuser is initialized. When the Docker container is started, it will ensure the `root` superuser is configured according to those environment variables.

In our example below, we're using `DOLT_ROOT_HOST` to override the host of the `root` superuser account to `%` in order to allow any host to connect to our server and log in as `root`. We're also using `DOLT_ROOT_PASSWORD` to override the default, empty password and specify a password for the `root` account. Setting a password is strongly advised for security when allowing the `root` account to connect from any host.

```bash
> docker run -e DOLT_ROOT_PASSWORD=secret2 -e DOLT_ROOT_HOST=% -p 3307:3306 dolthub/dolt-sql-server:latest
```

Now, you have a running server in the container, and we can connect to it by specifying our host, 3307 for the port, and root for the user,
since that's the default user and we didn't provide any configuration when running the server.
If we run the command above with -d or switch to a separate window we can connect with MySQL:

For example, you can run mysql client to connect to the server like this:
```shell
$ mysql --host 0.0.0.0 -P 3307 -u root
```bash
> mysql --host 0.0.0.0 -P 3307 -u root -p secret2
```

### Define configuration for the server

You can either define server configuration as commandline arguments, or you can use yaml configuration file.
For the commandline argument definition you can simply define arguments after whole docker command.
You can specify server configuration with commandline arguments, or you can use a YAML configuration file.
For commandline arguments, you can simply add arguments at the end of the docker command, as shown below.

```shell
$ docker run -p 3307:3306 dolthub/dolt-sql-server:latest -l debug --no-auto-commit
```

Or, we can mount a local directory to specific directories in the container.
To use a configuration file, you can map a local directory to location in the container.
The special directory for server configuration is `/etc/dolt/servercfg.d/`. You can only have one `.yaml` configuration
file in this directory. If there are multiple, the default configuration will be used. If the location of
configuration file was `/Users/jennifer/docker/server/config.yaml`, this is how to use `-v` flag which mounts
Expand All @@ -127,7 +126,7 @@ $ docker run -p 3307:3306 -v /Users/jennifer/docker/server/:/etc/dolt/servercfg.
The Dolt configuration and data directories can be configured similarly:

- The dolt configuration directory is `/etc/dolt/doltcfg.d/`
There should be one `.json` dolt configuration file. It will replace the global dolt configuration file in the
There should be one `.json` Dolt configuration file. It will replace the global Dolt configuration file in the
container.

- We set the location of where data to be stored to default location at `/var/lib/dolt/` in the container.
Expand Down
33 changes: 17 additions & 16 deletions go/cmd/dolt/commands/engine/sqlengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,23 @@ type contextFactory func(ctx context.Context, session sql.Session) (*sql.Context
type SystemVariables map[string]interface{}

type SqlEngineConfig struct {
IsReadOnly bool
IsServerLocked bool
DoltCfgDirPath string
PrivFilePath string
BranchCtrlFilePath string
ServerUser string
ServerPass string
ServerHost string
Autocommit bool
DoltTransactionCommit bool
Bulk bool
JwksConfig []servercfg.JwksConfig
SystemVariables SystemVariables
ClusterController *cluster.Controller
BinlogReplicaController binlogreplication.BinlogReplicaController
EventSchedulerStatus eventscheduler.SchedulerStatus
IsReadOnly bool
IsServerLocked bool
DoltCfgDirPath string
PrivFilePath string
BranchCtrlFilePath string
ServerUser string
ServerPass string
ServerHost string
SkipRootUserInitialization bool
Autocommit bool
DoltTransactionCommit bool
Bulk bool
JwksConfig []servercfg.JwksConfig
SystemVariables SystemVariables
ClusterController *cluster.Controller
BinlogReplicaController binlogreplication.BinlogReplicaController
EventSchedulerStatus eventscheduler.SchedulerStatus
}

// NewSqlEngine returns a SqlEngine
Expand Down
5 changes: 5 additions & 0 deletions go/cmd/dolt/commands/sqlserver/command_line_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ func (cfg *commandLineServerConfig) User() string {
return cfg.user
}

// UserIsSpecified returns true if the configuration explicitly specified a user.
func (cfg *commandLineServerConfig) UserIsSpecified() bool {
return cfg.user != ""
}

// Password returns the password that connecting clients must use.
func (cfg *commandLineServerConfig) Password() string {
return cfg.password
Expand Down
145 changes: 118 additions & 27 deletions go/cmd/dolt/commands/sqlserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
Expand All @@ -44,6 +45,7 @@ import (
"github.com/dolthub/dolt/go/cmd/dolt/commands/engine"
eventsapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi/v1alpha1"
remotesapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/remotesapi/v1alpha1"
"github.com/dolthub/dolt/go/libraries/doltcore/dconfig"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/env"
"github.com/dolthub/dolt/go/libraries/doltcore/remotesrv"
Expand All @@ -56,6 +58,7 @@ import (
"github.com/dolthub/dolt/go/libraries/doltcore/sqlserver"
"github.com/dolthub/dolt/go/libraries/events"
"github.com/dolthub/dolt/go/libraries/utils/config"
"github.com/dolthub/dolt/go/libraries/utils/filesys"
"github.com/dolthub/dolt/go/libraries/utils/svcs"
)

Expand All @@ -80,13 +83,14 @@ func Serve(
serverConfig servercfg.ServerConfig,
controller *svcs.Controller,
dEnv *env.DoltEnv,
skipRootUserInitialization bool,
) (startError error, closeError error) {
// Code is easier to work through if we assume that serverController is never nil
if controller == nil {
controller = svcs.NewController()
}

ConfigureServices(serverConfig, controller, version, dEnv)
ConfigureServices(serverConfig, controller, version, dEnv, skipRootUserInitialization)

go controller.Start(ctx)
err := controller.WaitForStart()
Expand All @@ -101,6 +105,7 @@ func ConfigureServices(
controller *svcs.Controller,
version string,
dEnv *env.DoltEnv,
skipRootUserInitialization bool,
) {
ValidateConfigStep := &svcs.AnonService{
InitF: func(context.Context) error {
Expand Down Expand Up @@ -219,19 +224,20 @@ func ConfigureServices(
InitSqlEngineConfig := &svcs.AnonService{
InitF: func(context.Context) error {
config = &engine.SqlEngineConfig{
IsReadOnly: serverConfig.ReadOnly(),
PrivFilePath: serverConfig.PrivilegeFilePath(),
BranchCtrlFilePath: serverConfig.BranchControlFilePath(),
DoltCfgDirPath: serverConfig.CfgDir(),
ServerUser: serverConfig.User(),
ServerPass: serverConfig.Password(),
ServerHost: serverConfig.Host(),
Autocommit: serverConfig.AutoCommit(),
DoltTransactionCommit: serverConfig.DoltTransactionCommit(),
JwksConfig: serverConfig.JwksConfig(),
SystemVariables: serverConfig.SystemVars(),
ClusterController: clusterController,
BinlogReplicaController: binlogreplication.DoltBinlogReplicaController,
IsReadOnly: serverConfig.ReadOnly(),
PrivFilePath: serverConfig.PrivilegeFilePath(),
BranchCtrlFilePath: serverConfig.BranchControlFilePath(),
DoltCfgDirPath: serverConfig.CfgDir(),
ServerUser: serverConfig.User(),
ServerPass: serverConfig.Password(),
ServerHost: serverConfig.Host(),
Autocommit: serverConfig.AutoCommit(),
DoltTransactionCommit: serverConfig.DoltTransactionCommit(),
JwksConfig: serverConfig.JwksConfig(),
SystemVariables: serverConfig.SystemVars(),
ClusterController: clusterController,
BinlogReplicaController: binlogreplication.DoltBinlogReplicaController,
SkipRootUserInitialization: skipRootUserInitialization,
}
return nil
},
Expand Down Expand Up @@ -363,30 +369,92 @@ func ConfigureServices(
}
controller.Register(InitBinlogging)

// Add superuser if specified user exists; add root superuser if no user specified and no existing privileges
InitSuperUser := &svcs.AnonService{
InitF: func(context.Context) error {
userSpecified := config.ServerUser != ""
// MySQL creates a root superuser when the mysql install is first initialized. Depending on the options
// specified, the root superuser is created without a password, or with a random password. This varies
// slightly in some OS-specific installers. Dolt initializes the root superuser the first time a
// sql-server is started and initializes its privileges database. We do this on sql-server initialization,
// instead of dolt db initialization, because we only want to create the privileges database when it's
// used for a server, and because we want the same root initialization logic when a sql-server is started
// for a clone. More details: https://dev.mysql.com/doc/mysql-security-excerpt/8.0/en/default-privileges.html
//
// NOTE: The MySQL root user is created for host 'localhost', not any host ('%'). We could do the same here,
// but it seems like it would cause problems for users who want to connect from outside of Docker.
InitImplicitRootSuperUser := &svcs.AnonService{
InitF: func(ctx context.Context) error {
// If privileges.db has already been initialized, indicating that this is NOT the
// first time sql-server has been launched, then don't initialize the root superuser.
if permissionDbExists, err := doesPrivilegesDbExist(dEnv, serverConfig.PrivilegeFilePath()); err != nil {
return err
} else if permissionDbExists {
logrus.Debug("privileges.db already exists, not creating root superuser")
return nil
}

// We always persist the privileges.db file, to signal that the privileges system has been initialized
mysqlDb := sqlEngine.GetUnderlyingEngine().Analyzer.Catalog.MySQLDb
ed := mysqlDb.Editor()
var numUsers int
ed.VisitUsers(func(*mysql_db.User) { numUsers += 1 })
privsExist := numUsers != 0
defer ed.Close()

// If no ephemeral superuser has been configured and root user initialization wasn't skipped,
// then create a root@localhost superuser.
if !serverConfig.UserIsSpecified() && !config.SkipRootUserInitialization {
// Allow the user to override the default root host (localhost) and password ("").
// This is particularly useful in a Docker container, where you need to connect
// to the sql-server from outside the container and can't rely on localhost.
rootHost := "localhost"
doltRootHost := os.Getenv(dconfig.EnvDoltRootHost)
if doltRootHost != "" {
logrus.Infof("Overriding root user host with value from DOLT_ROOT_HOST: %s", doltRootHost)
rootHost = doltRootHost
}

rootPassword := servercfg.DefaultPass
doltRootPassword := os.Getenv(dconfig.EnvDoltRootPassword)
if doltRootPassword != "" {
logrus.Info("Overriding root user password with value from DOLT_ROOT_PASSWORD")
rootPassword = doltRootPassword
}

logrus.Infof("Creating root@%s superuser", rootHost)
mysqlDb.AddSuperUser(ed, servercfg.DefaultUser, rootHost, rootPassword)
}

// TODO: The in-memory filesystem doesn't work with the GMS API
// for persisting the privileges database. The filesys API
// is in the Dolt layer, so when the file path is passed to
// GMS, it expects it to be a path on disk, and errors out.
if _, isInMemFs := dEnv.FS.(*filesys.InMemFS); isInMemFs {
return nil
} else {
sqlCtx, err := sqlEngine.NewDefaultContext(context.Background())
if err != nil {
return err
}
return mysqlDb.Persist(sqlCtx, ed)
}
},
}
controller.Register(InitImplicitRootSuperUser)

// Add an ephemeral superuser if one was requested
InitEphemeralSuperUser := &svcs.AnonService{
InitF: func(context.Context) error {
mysqlDb := sqlEngine.GetUnderlyingEngine().Analyzer.Catalog.MySQLDb
ed := mysqlDb.Editor()

userSpecified := config.ServerUser != ""
if userSpecified {
superuser := mysqlDb.GetUser(ed, config.ServerUser, "%", false)
if userSpecified && superuser == nil {
mysqlDb.AddSuperUser(ed, config.ServerUser, "%", config.ServerPass)
if superuser == nil {
mysqlDb.AddEphemeralSuperUser(ed, config.ServerUser, "%", config.ServerPass)
}
} else if !privsExist {
mysqlDb.AddSuperUser(ed, servercfg.DefaultUser, "%", servercfg.DefaultPass)
}
ed.Close()

return nil
},
}
controller.Register(InitSuperUser)
controller.Register(InitEphemeralSuperUser)

var metListener *metricsListener
InitMetricsListener := &svcs.AnonService{
Expand All @@ -406,7 +474,7 @@ func ConfigureServices(
InitF: func(context.Context) error {
mysqlDb := sqlEngine.GetUnderlyingEngine().Analyzer.Catalog.MySQLDb
ed := mysqlDb.Editor()
mysqlDb.AddSuperUser(ed, LocalConnectionUser, "localhost", localCreds.Secret)
mysqlDb.AddEphemeralSuperUser(ed, LocalConnectionUser, "localhost", localCreds.Secret)
ed.Close()
return nil
},
Expand Down Expand Up @@ -859,6 +927,29 @@ func (r *remotesapiAuth) ApiAuthorize(ctx context.Context, superUserRequired boo
return true, nil
}

// doesPrivilegesDbExist looks for an existing privileges database as the specified |privilegeFilePath|. If
// |privilegeFilePath| is an absolute path, it is used directly. If it is a relative path, then it is resolved
// relative to the root of the specified |dEnv|.
func doesPrivilegesDbExist(dEnv *env.DoltEnv, privilegeFilePath string) (exists bool, err error) {
if !filepath.IsAbs(privilegeFilePath) {
privilegeFilePath, err = dEnv.FS.Abs(privilegeFilePath)
if err != nil {
return false, err
}
}

_, err = os.Stat(privilegeFilePath)
if err != nil {
if os.IsNotExist(err) {
return false, nil
} else {
return false, err
}
}

return true, nil
}

func LoadClusterTLSConfig(cfg servercfg.ClusterConfig) (*tls.Config, error) {
rcfg := cfg.RemotesAPIConfig()
if rcfg.TLSKey() == "" && rcfg.TLSCert() == "" {
Expand Down
Loading

0 comments on commit 427343e

Please sign in to comment.