Skip to content

Commit

Permalink
feat: configurable superuser password (#306)
Browse files Browse the repository at this point in the history
* feat: configurable superuser password
* Disable auth for empty superuser passwords
* Add test workflow
  • Loading branch information
fanyang01 authored Dec 20, 2024
1 parent ac2f57c commit be912e8
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 34 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/clients-compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ jobs:

- name: Start MyDuck Server
run: |
./myduckserver &
sleep 5
./myduckserver --flightsql-port 47470 &
sleep 10
- name: Run the Compatibility Test for FlightSQL
run: |
Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/password-auth.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Password Auth Test

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Install system packages
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: postgresql-client mysql-client
version: 1.0

- name: Install dependencies
run: |
go get .
pip3 install "sqlglot[rs]"
curl -LJO https://github.com/duckdb/duckdb/releases/latest/download/duckdb_cli-linux-amd64.zip
unzip duckdb_cli-linux-amd64.zip
chmod +x duckdb
sudo mv duckdb /usr/local/bin
- name: Build
run: go build -v

- name: Start MyDuck Server with password
run: |
./myduckserver --superuser-password=testpass123 &
sleep 5
- name: Test PostgreSQL auth
run: |
export PGPASSWORD=testpass123
# Basic connection test
psql -h 127.0.0.1 -U postgres -d postgres -c "SELECT 1 as test;"
# Create and query a table
psql -h 127.0.0.1 -U postgres -d postgres -c "CREATE TABLE test (id int); INSERT INTO test VALUES (42); SELECT * FROM test;"
# Test wrong password
! PGPASSWORD=wrongpass psql -h 127.0.0.1 -U postgres -d postgres -c "SELECT 1"
- name: Test MySQL auth
run: |
# Basic connection test
mysql -h127.0.0.1 -uroot -ptestpass123 -e "SELECT 1 as test;"
# Create and query a table
mysql -h127.0.0.1 -uroot -ptestpass123 -e "CREATE DATABASE IF NOT EXISTS test; USE test; CREATE TABLE t1 (id int); INSERT INTO t1 VALUES (42); SELECT * FROM t1;"
# Test wrong password
! mysql -h127.0.0.1 -uroot -pwrongpass -e "SELECT 1"
4 changes: 1 addition & 3 deletions .github/workflows/psql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,10 @@ jobs:
pip3 install "sqlglot[rs]" pyarrow pandas
curl -LJO https://github.com/duckdb/duckdb/releases/download/v1.1.3/duckdb_cli-linux-amd64.zip
curl -LJO https://github.com/duckdb/duckdb/releases/latest/download/duckdb_cli-linux-amd64.zip
unzip duckdb_cli-linux-amd64.zip
chmod +x duckdb
sudo mv duckdb /usr/local/bin
duckdb -c 'INSTALL json from core'
duckdb -c 'SELECT extension_name, loaded, install_path FROM duckdb_extensions() where installed'
- name: Build
run: go build -v
Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,18 +163,34 @@ With MyDuck's powerful analytics capabilities, you can create an hybrid transact
To rename the default database, pass the `DEFAULT_DB` environment variable to the Docker container:

```bash
docker run -p 13306:3306 -p 15432:5432 --env=DEFAULT_DB=mydbname apecloud/myduckserver:latest
docker run -d -p 13306:3306 -p 15432:5432 \
--env=DEFAULT_DB=mydbname \
apecloud/myduckserver:latest
```


To set the superuser password, pass the `SUPERUSER_PASSWORD` environment variable to the Docker container:

```bash
docker run -d -p 13306:3306 -p 15432:5432 \
--env=SUPERUSER_PASSWORD=mysecretpassword \
apecloud/myduckserver:latest
```


To initialize MyDuck Server with custom SQL statements, mount your `.sql` file to either `/docker-entrypoint-initdb.d/mysql/` or `/docker-entrypoint-initdb.d/postgres/` inside the Docker container, depending on the SQL dialect you're using.

For example:
```bash
# Execute `init.sql` via MySQL protocol
docker run -d -p 13306:3306 --name=myduck -v ./init.sql:/docker-entrypoint-initdb.d/mysql/init.sql apecloud/myduckserver:latest
docker run -d -p 13306:3306 --name=myduck \
-v ./init.sql:/docker-entrypoint-initdb.d/mysql/init.sql \
apecloud/myduckserver:latest

# Execute `init.sql` via PostgreSQL protocol
docker run -d -p 15432:5432 --name=myduck -v ./init.sql:/docker-entrypoint-initdb.d/postgres/init.sql apecloud/myduckserver:latest
docker run -d -p 15432:5432 --name=myduck \
-v ./init.sql:/docker-entrypoint-initdb.d/postgres/init.sql \
apecloud/myduckserver:latest
```

### Query Parquet Files
Expand Down
2 changes: 1 addition & 1 deletion devtools/replica-setup-postgres/replica_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ usage() {

MYDUCK_HOST=${MYDUCK_HOST:-127.0.0.1}
MYDUCK_PORT=${MYDUCK_PORT:-5432}
MYDUCK_USER=${MYDUCK_USER:-mysql}
MYDUCK_USER=${MYDUCK_USER:-postgres}
MYDUCK_PASSWORD=${MYDUCK_PASSWORD:-}
MYDUCK_SERVER_ID=${MYDUCK_SERVER_ID:-2}
MYDUCK_IN_DOCKER=${MYDUCK_IN_DOCKER:-false}
Expand Down
6 changes: 5 additions & 1 deletion docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ run_replica_setup() {

run_server_in_background() {
cd "$DATA_PATH" || { echo "Error: Could not change directory to ${DATA_PATH}"; exit 1; }
nohup myduckserver $DEFAULT_DB $LOG_LEVEL $PROFILER_PORT $RESTORE_FILE $RESTORE_ENDPOINT $RESTORE_ACCESS_KEY_ID $RESTORE_SECRET_ACCESS_KEY|tee -a "${LOG_PATH}/server.log" 2>&1 &
nohup myduckserver $DEFAULT_DB $SUPERUSER_PASSWORD $LOG_LEVEL $PROFILER_PORT $RESTORE_FILE $RESTORE_ENDPOINT $RESTORE_ACCESS_KEY_ID $RESTORE_SECRET_ACCESS_KEY | tee -a "${LOG_PATH}/server.log" 2>&1 &
echo "$!" > "${PID_FILE}"
}

Expand Down Expand Up @@ -207,6 +207,10 @@ setup() {
export DEFAULT_DB="--default-db=$DEFAULT_DB"
fi

if [ -n "$SUPERUSER_PASSWORD" ]; then
export SUPERUSER_PASSWORD="--superuser-password=$SUPERUSER_PASSWORD"
fi

if [ -n "$LOG_LEVEL" ]; then
export LOG_LEVEL="--loglevel=$LOG_LEVEL"
fi
Expand Down
10 changes: 8 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ var (

postgresPort = 5432

// Shared between the MySQL and Postgres servers.
superuserPassword = ""

defaultTimeZone = ""

// for Restore
Expand All @@ -68,7 +71,7 @@ var (
restoreSecretAccessKey = ""

flightsqlHost = "localhost"
flightsqlPort = 47470
flightsqlPort = -1 // Disabled by default
)

func init() {
Expand All @@ -81,6 +84,8 @@ func init() {
flag.StringVar(&defaultDb, "default-db", defaultDb, "The default database name to use.")
flag.IntVar(&logLevel, "loglevel", logLevel, "The log level to use.")

flag.StringVar(&superuserPassword, "superuser-password", superuserPassword, "The password for the superuser account.")

flag.StringVar(&replicaOptions.ReportHost, "report-host", replicaOptions.ReportHost, "The host name or IP address of the replica to be reported to the source during replica registration.")
flag.IntVar(&replicaOptions.ReportPort, "report-port", replicaOptions.ReportPort, "The TCP/IP port number for connecting to the replica, to be reported to the source during replica registration.")
flag.StringVar(&replicaOptions.ReportUser, "report-user", replicaOptions.ReportUser, "The account user name of the replica to be reported to the source during replica registration.")
Expand Down Expand Up @@ -154,7 +159,7 @@ func main() {
engine.Analyzer.Catalog.RegisterFunction(sql.NewContext(context.Background()), myfunc.ExtraBuiltIns...)
engine.Analyzer.Catalog.MySQLDb.SetPlugins(plugin.AuthPlugins)

if err := setPersister(provider, engine); err != nil {
if err := setPersister(provider, engine, "root", superuserPassword); err != nil {
logrus.Fatalln("Failed to set the persister:", err)
}

Expand Down Expand Up @@ -182,6 +187,7 @@ func main() {
pgServer, err := pgserver.NewServer(
provider, pool,
address, postgresPort,
superuserPassword,
func() *sql.Context {
session := backend.NewSession(memory.NewSession(sql.NewBaseSession(), provider), provider, pool)
return sql.NewContext(context.Background(), sql.WithSession(session))
Expand Down
8 changes: 4 additions & 4 deletions persist.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (m *MySQLPersister) Persist(ctx *sql.Context, data []byte) error {
}

// https://github.com/dolthub/go-mysql-server/blob/main/_example/users_example.go
func setPersister(provider sql.DatabaseProvider, engine *sqle.Engine) error {
func setPersister(provider sql.DatabaseProvider, engine *sqle.Engine, superuser, password string) error {
session := memory.NewSession(sql.NewBaseSession(), provider)
ctx := sql.NewContext(context.Background(), sql.WithSession(session))
ctx.SetCurrentDatabase("mysql")
Expand Down Expand Up @@ -67,16 +67,16 @@ func setPersister(provider sql.DatabaseProvider, engine *sqle.Engine) error {
}
}

addAccount := func(account string, address string) {
addAccount := func(account, password, address string) {
ed := mysqlDb.Editor()
defer ed.Close()
mysqlDb.AddSuperUser(ed, account, address, "")
mysqlDb.AddSuperUser(ed, account, address, password)
}

// Modify it to "%" to allow accepting connections outside when myduckserver runs in Docker
// TODO should add a config to decide this or some better way to support this
// addAccount("root", "localhost")
addAccount("root", "%")
addAccount(superuser, password, "%")

return nil
}
32 changes: 16 additions & 16 deletions pgserver/authentication_scram.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"encoding/base64"
"fmt"
"net"
"os"
"strings"

"github.com/dolthub/doltgresql/server/auth"
Expand All @@ -39,26 +38,27 @@ const (
)

// EnableAuthentication handles whether authentication is enabled. If enabled, it verifies that the given user exists,
// and checks that the encrypted password is derivable from the stored encrypted password. As the feature is still in
// development, it is disabled by default. It may be enabled by supplying the environment variable
// "DOLTGRES_ENABLE_AUTHENTICATION", or by simply setting this boolean to true.
var EnableAuthentication = false

func init() {
if _, ok := os.LookupEnv("DOLTGRES_ENABLE_AUTHENTICATION"); ok {
EnableAuthentication = true
}
// and checks that the encrypted password is derivable from the stored encrypted password.
var EnableAuthentication = true

func InitSuperuser(password string) {
auth.DropRole("doltgres")
auth.DropRole("postgres")

var err error
mysql := auth.CreateDefaultRole("mysql")
mysql.CanLogin = true
mysql.Password, err = auth.NewScramSha256Password("")
postgres := auth.CreateDefaultRole("postgres")
postgres.CanLogin = true
postgres.Password, err = auth.NewScramSha256Password(password)
if err != nil {
panic(err)
}
auth.SetRole(mysql)
auth.SetRole(postgres)

// Postgres does not allow empty passwords,
// so we disable authentication if the superuser password is empty.
if password == "" {
EnableAuthentication = false
}
}

// SASLBindingFlag are the flags for gs2-cbind-flag, used in SASL authentication.
Expand Down Expand Up @@ -111,15 +111,15 @@ func (h *ConnectionHandler) handleAuthentication(startupMessage *pgproto3.Startu
}
}
} else {
username = "doltgres" // TODO: should we use this, or the default "postgres" since programs may default to it?
username = "postgres"
host = "localhost"
}
h.mysqlConn.User = username
h.mysqlConn.UserData = sql.MysqlConnectionUser{
User: username,
Host: host,
}
// Since this is all still in development, we'll check if authentication is enabled.
// Currently, regression tests disable authentication, since we can't just replay the messages due to nonces.
if !EnableAuthentication {
return h.send(&pgproto3.AuthenticationOk{})
}
Expand Down
4 changes: 3 additions & 1 deletion pgserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package pgserver

import (
"fmt"

"github.com/apecloud/myduckserver/backend"
"github.com/apecloud/myduckserver/catalog"
"github.com/dolthub/go-mysql-server/server"
Expand All @@ -16,7 +17,8 @@ type Server struct {
NewInternalCtx func() *sql.Context
}

func NewServer(provider *catalog.DatabaseProvider, connPool *backend.ConnectionPool, host string, port int, newCtx func() *sql.Context, options ...ListenerOpt) (*Server, error) {
func NewServer(provider *catalog.DatabaseProvider, connPool *backend.ConnectionPool, host string, port int, password string, newCtx func() *sql.Context, options ...ListenerOpt) (*Server, error) {
InitSuperuser(password)
addr := fmt.Sprintf("%s:%d", host, port)
l, err := server.NewListener("tcp", addr, "")
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion pgtest/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func CreateTestServer(t *testing.T, port int) (ctx context.Context, pgServer *pg
pgServer, err = pgserver.NewServer(
provider, pool,
"127.0.0.1", port,
"",
func() *sql.Context {
session := backend.NewSession(memory.NewSession(sql.NewBaseSession(), provider), provider, pool)
return sql.NewContext(context.Background(), sql.WithSession(session))
Expand All @@ -79,7 +80,7 @@ func CreateTestServer(t *testing.T, port int) (ctx context.Context, pgServer *pg
}

// Since we use the in-memory DuckDB storage, we need to connect to the `memory` database
dsn := fmt.Sprintf("postgres://mysql:@127.0.0.1:%d/memory", port)
dsn := fmt.Sprintf("postgres://postgres:@127.0.0.1:%d/memory", port)
conn, err = pgx.Connect(ctx, dsn)
if err != nil {
close()
Expand Down

0 comments on commit be912e8

Please sign in to comment.