diff --git a/.github/workflows/test-docker-local-image.yaml b/.github/workflows/test-docker-local-image.yaml index 467ecdb9f..e4e251601 100644 --- a/.github/workflows/test-docker-local-image.yaml +++ b/.github/workflows/test-docker-local-image.yaml @@ -6,11 +6,7 @@ on: - docker/Dockerfile.local - scripts/install_local_deps.sh - .github/workflows/test-docker-local-image.yaml - merge_group: - paths: - - docker/Dockerfile.local - - scripts/install_local_deps.sh - - .github/workflows/test-docker-local-image.yaml + - scripts/test_docker_local.sh push: branches: - main @@ -24,15 +20,5 @@ jobs: with: submodules: true - - name: Build local image - run: make docker-local - - - name: Run local image - run: docker run -d -p 8081:8081 tigris_local - - - name: Run CLI tests - run: | - curl -sSL https://tigris.dev/cli-linux | tar -xz -C . - TIGRIS_URL=localhost:8081 ./tigris ping --timeout 20s - TIGRIS_TEST_PORT=8081 TIGRIS_CLI_TEST_FAST=1 noup=1 /bin/bash test/v1/cli/main.sh - + - name: Run tests + run: SUDO=sudo /bin/bash scripts/test_docker_local.sh diff --git a/Makefile b/Makefile index 882393afa..e298a12f5 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ local_test: generate lint local_run: server $(DOCKER_COMPOSE) up --no-build --detach tigris_search tigris_db2 tigris_cache fdbcli -C ./test/config/fdb.cluster --exec "configure new single memory" || true - ./server/service -c config/server.dev.yaml + ./server/service -c test/config/server.dev.yaml # Start local instance with server running on the host in realtime mode. # This is useful for debugging the server. The process is attachable from IDE. diff --git a/docker/Dockerfile b/docker/Dockerfile index 403da0fcc..69c80dcf2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -41,9 +41,10 @@ RUN groupadd -r tigris && useradd -r -s /bin/false -g tigris tigris RUN mkdir -p /server /etc/tigrisdata/tigris /etc/foundationdb/ ARG BUILD_PROFILE="" +ARG CONF_PATH="" COPY --from=build /build/server/service /server/service -COPY --from=build /build/config/server${BUILD_PROFILE}.yaml /etc/tigrisdata/tigris +COPY --from=build /build/${CONF_PATH}config/server${BUILD_PROFILE}.yaml /etc/tigrisdata/tigris COPY --from=build /usr/lib/libfdb_c.so /usr/lib/libfdb_c.so COPY --from=build /usr/bin/fdbcli /usr/bin/fdbcli diff --git a/docker/Dockerfile.local b/docker/Dockerfile.local index 7721413eb..c8dedf9ff 100644 --- a/docker/Dockerfile.local +++ b/docker/Dockerfile.local @@ -20,11 +20,13 @@ RUN go mod download COPY . /build RUN --mount=type=cache,target=/root/.cache/go-build rm -f server/service && make bins +RUN go install -tags tigris_http,tigris_grpc -ldflags "-w -s" github.com/tigrisdata/gotrue@latest + FROM ubuntu:20.04 AS server RUN apt-get update && \ apt-get install -y --no-install-recommends \ - ca-certificates \ + ca-certificates openssh-client jq \ curl && apt-get clean COPY scripts/install_local_docker_deps.sh /tmp/ @@ -35,15 +37,16 @@ RUN rm -rf /etc/apt/* /var/lib/dpkg/* /var/lib/apt/* # Setup an unprivileged user RUN groupadd -r tigris && useradd -r -s /bin/false -g tigris tigris -RUN mkdir -p /server /etc/tigrisdata/tigris /etc/foundationdb /var/lib/foundationdb/logs +RUN mkdir -p /server /etc/tigrisdata/tigris COPY --from=build /build/server/service /server/service COPY --from=build /build/config/server.yaml /etc/tigrisdata/tigris COPY --from=build /usr/lib/libfdb_c.so /usr/lib/libfdb_c.so COPY --from=build /usr/bin/fdbcli /usr/bin/fdbcli +COPY --from=build /root/go/bin/gotrue /usr/bin/gotrue RUN chown -R tigris:tigris /server /etc/tigrisdata/tigris -COPY docker/service-local.sh /server/service.sh +COPY scripts/service-local.sh /server/service.sh EXPOSE 8081 diff --git a/docker/service-local.sh b/docker/service-local.sh deleted file mode 100644 index f09609ec5..000000000 --- a/docker/service-local.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -function start_typesense() { - /usr/bin/typesense-server --config=/etc/typesense/typesense-server.ini & -} - -function wait_for_typesense() { - echo "Waiting for typesense to start" - IS_LEADER=1 - echo "Waiting for typesense to become leader" - while [ ${IS_LEADER} -ne 0 ] - do - curl -H "X-TYPESENSE-API-KEY: ${TIGRIS_SERVER_SEARCH_AUTH_KEY}" localhost:8108/status | grep LEADER - IS_LEADER=$? - if [ ${IS_LEADER} -ne 0 ] - then - echo "Typesense is not leader yet, waiting" - fi - sleep 2 - done - - echo "Waiting for typesense to respond to list collections" - LIST_COLLECTIONS_RESP=1 - while [ ${LIST_COLLECTIONS_RESP} -ne 0 ] - do - # Try to do list collections and see the response code, this can take time - curl -H "X-TYPESENSE-API-KEY: ${TIGRIS_SERVER_SEARCH_AUTH_KEY}" -I -X GET localhost:8108/collections | grep "200 OK" - LIST_COLLECTIONS_RESP=$? - if [ ${LIST_COLLECTIONS_RESP} -ne 0 ] - then - echo "Typesense could not list collections yet, waiting" - fi - sleep 2 - done -} - -function start_fdb() { - fdbserver --listen-address 127.0.0.1:4500 --public-address 127.0.0.1:4500 --datadir /var/lib/foundationdb/data --logdir /var/lib/foundationdb/logs --locality-zoneid tigris --locality-machineid tigris & - fdbcli --exec 'configure new single memory' -} - -function wait_for_fdb() { - echo "Waiting for foundationdb to start" - OUTPUT="something_else" - while [ "x${OUTPUT}" != "xThe database is available." ] - do - OUTPUT=$(fdbcli --exec 'status minimal') - sleep 2 - done -} - -export TIGRIS_SERVER_SERVER_TYPE=database -export TIGRIS_SERVER_SEARCH_AUTH_KEY=ts_dev_key -export TIGRIS_SERVER_SEARCH_HOST=localhost -export TIGRIS_SERVER_CDC_ENABLED=true - -start_fdb -wait_for_fdb -start_typesense -wait_for_typesense -/server/service diff --git a/scripts/install_local_docker_deps.sh b/scripts/install_local_docker_deps.sh index 1afa39ed1..4cf68f95b 100644 --- a/scripts/install_local_docker_deps.sh +++ b/scripts/install_local_docker_deps.sh @@ -13,9 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. - set -ex +D=/var/lib/tigris + ARCH=$(dpkg --print-architecture) case "${ARCH}" in @@ -69,5 +70,7 @@ curl --create-dirs -Lo "$TS_PACKAGE_PATH" "https://dl.typesense.org/releases/${T dpkg --unpack "$TS_PACKAGE_PATH" rm -f /var/lib/dpkg/info/typesense-server.postinst dpkg --configure typesense-server -sed -i "s/\$API_KEY/ts_dev_key/g" /etc/typesense/typesense-server.ini && \ -rm -f "$TS_PACKAGE_PATH" +rm -rf /var/lib/typesense /etc/typesense + +mkdir $D + diff --git a/scripts/service-local.sh b/scripts/service-local.sh new file mode 100644 index 000000000..133537ac9 --- /dev/null +++ b/scripts/service-local.sh @@ -0,0 +1,412 @@ +#!/bin/bash + +set -e + +if [ -n "$TIGRIS_LOCAL_DEBUG" ]; then + set -x +fi + +D=/var/lib/tigris + +function http_post_json() { + curl -s --fail --location --request POST "$1" \ + --header 'X-JWT-AUD: https://tigris-local-aud' \ + --header 'Content-Type: application/json' \ + --data-raw "$2" +} + +function http_post_form() { + curl -s --fail --location --request POST "$1" \ + --header 'X-JWT-AUD: https://tigris-local-aud' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode "$2" \ + --data-urlencode "$3" +} + +function start_typesense() { + /usr/bin/typesense-server --config=$D/typesense/config/typesense-server.ini & +} + +function health() { + while ! curl -s --fail "$1/health"; do + echo "Waiting for health at $1" + sleep 1 + done +} + +function expect_typesense() { + while ! curl -s --fail -H "X-TYPESENSE-API-KEY: ${SEARCH_AUTH_KEY}" "localhost:8108/$1" | grep "$2"; do + echo "Waiting for typesense $1 become $2" + sleep 1 + done +} + +function wait_for_typesense() { + echo "Waiting for typesense to start" + + SEARCH_AUTH_KEY=$(grep "api-key = " $D/typesense/config/typesense-server.ini|sed 's/api-key = //g') + + expect_typesense status LEADER + expect_typesense collections "" + + echo "Finished waiting Typesense to start" +} + +function start_fdb() { + fdbserver --listen-address 127.0.0.1:4500 --public-address 127.0.0.1:4500 \ + --datadir $D/foundationdb/data --logdir $D/foundationdb/logs \ + --locality-zoneid tigris --locality-machineid tigris & +} + +function start_gotrue() { + echo "Starting GoTrue" + /usr/bin/gotrue -c $D/gotrue/config/.env 2>>$D/gotrue/logs/stderr 1>>$D/gotrue/logs/stdout & + health http://localhost:8086 + echo "Started GoTrue" +} + +function start_server() { + /server/service -c $D/server/config/server.yaml 2>>$D/server/logs/stderr 1>>$D/server/logs/stdout & +} + +function signup_user() { + http_post_json localhost:8086/signup '{ + "email": "'"$1"'", + "password": "'"$2"'", + "app_data": { + "tigris_namespace": "'"$3"'" + } + }' 2>/dev/null 1>/dev/null +} + +# Create symmetric token to auth GoTrue on Tigris +# Shared secret GOTRUE_JWT_SECRET is used to validate this token by Tigris server +function get_hs256_token() { + # temporary server instance is started on 9081 + GOTRUE_DB_URL=http://localhost:9081 \ + GOTRUE_JWT_ALGORITHM=HS256 GOTRUE_JWT_EXP=315360000 /usr/bin/gotrue 2>/dev/null 1>/dev/null & + gpid=$! + + health http://localhost:8086 + + k=$(gen_key) + + signup_user "tigris_gotrue@m2m.tigrisdata.com" "$k" tigris_gotrue + + server_admin_token=$(http_post_form 'localhost:8086/token?grant_type=password' \ + 'username=tigris_gotrue@m2m.tigrisdata.com' \ + 'password='"$k"''| jq -r .access_token) + + kill $gpid + wait $gpid +} + +# Create user admin token which will be propagated to the host +# to authenticate local instance administrator +function get_user_admin_token() { + GOTRUE_DB_URL=http://localhost:9081 \ + GOTRUE_JWT_ALGORITHM=RS256 GOTRUE_JWT_EXP=315360000 /usr/bin/gotrue 2>/dev/null 1>/dev/null & + gpid=$! + + health http://localhost:8086 + + k=$(gen_key) + + signup_user "user_admin@m2m.tigrisdata.com" "$k" default_namespace + + user_admin_token=$(http_post_form 'localhost:8086/token?grant_type=password' \ + 'username=user_admin@m2m.tigrisdata.com' \ + 'password='"$k"''| jq -r .access_token) + + echo "$user_admin_token" >$D/user_admin_token.txt + + kill $gpid + wait $gpid +} + +function create_admin_namespaces() { + http_post_json localhost:9081/v1/management/namespaces/create \ + '{ "name": "tigris_gotrue", "id": "tigris_gotrue" }' + +# http_post_json localhost:9081/v1/management/namespaces/create \ +# '{ "name": "default_namespace", "id": "default_namespace" }' +} + +function create_tokens_and_namespaces() { + # start on different port so it's not visible outside docker + TIGRIS_SERVER_SERVER_PORT=9081 start_server # start unauthenticated server instance + spid=$! + + get_hs256_token + + if [ -n "$TIGRIS_LOCAL_GENERATE_ADMIN_TOKEN" ]; then + get_user_admin_token + fi + + create_admin_namespaces + + kill $spid + wait $spid +} + +# generate random password +function gen_key() { + head -c 256 /dev/urandom | md5sum | cut -f 1 -d ' ' +} + +function bootstrap_auth_post() { + echo -e "\nBootstrapping auth post" + + if [ -z "$INITIALIZING" ]; then + echo "Already initialized" + return + fi + + # Create super user to access GoTrue from Tigris auth provider + # shellcheck disable=SC2153 + /usr/bin/gotrue admin --instance_id="00000000-0000-0000-0000-000000000000" --aud="$GOTRUE_SUPERADMIN_AUD" --superadmin=true createuser "$GOTRUE_SUPERADMIN_USERNAME" "$GOTRUE_SUPERADMIN_PASSWORD" 2>/dev/null 1>/dev/null + + echo "Finished bootstrapping auth post" +} + +function bootstrap_auth_pre() { + echo -e "\nBootstrapping auth" + + if [ -z "$INITIALIZING" ]; then + echo "Already initialized" + return + fi + + ssh-keygen -m PEM -t rsa -b 4096 -f $D/gotrue/config/key -N "" -C "localhost:8086" >/dev/null + ssh-keygen -e -m pkcs8 -f $D/gotrue/config/key.pub >$D/gotrue/config/key_pem.pub + + gotrue_secret=$(gen_key) + gotrue_jwt_secret=$(gen_key) + gotrue_superadmin_password=$(gen_key) + gotrue_db_encryption_key=$(gen_key) + + cat <$D/gotrue/config/.env +GOTRUE_SECRET=$gotrue_secret +PORT=8086 +GOTRUE_MAILER_AUTOCONFIRM=true +GOTRUE_DB_PROJECT=tigris_gotrue +GOTRUE_DB_URL=http://localhost:8081 +GOTRUE_SITE_URL=http://localhost:8086 +GOTRUE_JWT_ALGORITHM=RS256 +GOTRUE_JWT_RSA_PRIVATE_KEY=$D/gotrue/config/key +GOTRUE_JWT_RSA_PUBLIC_KEYS=$D/gotrue/config/key_pem.pub +GOTRUE_DISABLE_SIGNUP=false +GOTRUE_JWT_EXP=300 +GOTRUE_JWT_ISSUER=http://localhost:8086 +GOTRUE_DB_BRANCH=main +GOTRUE_DB_ENCRYPTION_KEY=$gotrue_db_encryption_key +GOTRUE_OPERATOR_TOKEN=unused_token +GOTRUE_JWT_SECRET=$gotrue_jwt_secret +JWT_DEFAULT_GROUP_NAME=user +CREATE_SUPER_ADMIN_USER=true +GOTRUE_INSTANCE_ID=00000000-0000-0000-0000-000000000000 +GOTRUE_SUPERADMIN_AUD=https://tigris-local-aud +GOTRUE_SUPERADMIN_USERNAME=tigris_server@m2m.tigrisdata.com +GOTRUE_SUPERADMIN_PASSWORD=$gotrue_superadmin_password +GOTRUE_LOG_FORMAT=json +TIGRIS_SKIP_LOCAL_TLS=1 +EOF + + lvl="error" + if [ -n "$TIGRIS_LOCAL_DEBUG" ]; then + lvl="debug" + fi + + cat <>$D/gotrue/config/.env +GOTRUE_LOG_LEVEL=$lvl +EOF + + set -o allexport + # shellcheck disable=SC1091,SC1090 + source $D/gotrue/config/.env + set +o allexport + + create_tokens_and_namespaces + + cat <>$D/gotrue/config/.env +GOTRUE_DB_TOKEN=$server_admin_token + +EOF + + export GOTRUE_DB_TOKEN=$server_admin_token + + cat <>$D/server/config/server.yaml +auth: + enabled: true + authz: + enabled: true + log_only: false + enable_namespace_isolation: true + enable_oauth: true + oauth_provider: gotrue + log_only: false + admin_namespaces: + - default_namespace + - tigris_gotrue + validators: + - issuer: http://localhost:8086 + algorithm: RS256 + audience: https://tigris-local-aud + - issuer: http://localhost:8086 + algorithm: HS256 + audience: https://tigris-local-aud + token_cache_size: 100 + primary_audience: https://tigris-local-aud + gotrue: + username_suffix: "@m2m.tigrisdata.com" + url: http://localhost:8086 + admin_username: tigris_server@m2m.tigrisdata.com + admin_password: $gotrue_superadmin_password + shared_secret: $gotrue_jwt_secret +EOF + + echo "Finished bootstrapping auth" +} + +# this starts bootstrapping process only if the destination directory is empty +function bootstrap_pre() { + echo "Bootstraping the instance (pre)" + + if [ -z "$INITIALIZING" ]; then + echo "Already initialized" + return + fi + + mkdir -p $D/foundationdb/{logs,data} + mkdir -p $D/typesense/{config,logs,data} + mkdir -p $D/gotrue/{config,logs,data} + mkdir -p $D/server/{config,logs,data} + + SEARCH_AUTH_KEY=$(gen_key) + + cat <$D/typesense/config/typesense-server.ini +[server] + +api-address = 0.0.0.0 +api-port = 8108 +data-dir = $D/typesense/data +api-key = $SEARCH_AUTH_KEY +log-dir = $D/typesense/logs +EOF + + cat <$D/server/config/server.yaml +server: + port: 8081 + type: database + unix_socket: /var/lib/tigris/server/unix.sock +environment: production +search: + auth_key: $SEARCH_AUTH_KEY + host: localhost + chunking: true + compression: true +kv: + chunking: true + compression: true + min_compression_threshold: 1 +log: + level: debug +secondary_index: + write_enabled: true + read_enabled: true + mutate_enabled: true +tracing: + enabled: false +metrics: + enabled: false +EOF + + echo "Finished bootstrapping the instance (pre)" +} + +# continues bootstrapping process after starting FDB and Typesense +function bootstrap_post() { + echo "Bootstraping the instance (post)" + + if [ -z "$INITIALIZING" ]; then + echo "Already initialized" + return + fi + + if [ -n "$TIGRIS_LOCAL_PERSISTENCE" ]; then + fdbcli --exec 'configure new single ssd' + else + fdbcli --exec 'configure new single memory' + fi + + date > /var/lib/tigris/initialized + + echo "Finished bootstraping the instance (post)" +} + +function wait_for_fdb() { + echo "Waiting for foundationdb to start" + + OUTPUT="something_else" + while [ "x${OUTPUT}" != "xThe database is available." ] + do + OUTPUT=$(fdbcli --exec 'status minimal') + sleep 1 + done + + echo "Finished waiting for foundationdb to start" +} + +main() { + date + echo "Starting Tigris instance" + + # shellcheck disable=SC2010,SC2143 + if [ -n "$(ls -A $D|grep -v init.log)" ]; then + echo "Already initialized" + unset INITIALIZING + else + INITIALIZING=1 + fi + + bootstrap_pre + + start_fdb + start_typesense + + bootstrap_post + + wait_for_fdb + wait_for_typesense + + if [ -n "$TIGRIS_BOOTSTRAP_LOCAL_AUTH" ]; then + bootstrap_auth_pre + fi + + if [ -n "$TIGRIS_SKIP_LOCAL_AUTH" ]; then + export TIGRIS_SERVER_AUTH_ENABLED=false + export TIGRIS_SERVER_AUTH_ENABLE_NAMESPACE_ISOLATION=false + echo "Starting with authentication disabled" + fi + + start_server + pid=$! + + health "http://localhost:8081/v1" + + if [ -n "$TIGRIS_BOOTSTRAP_LOCAL_AUTH" ]; then + bootstrap_auth_post + fi + + if [ -f $D/gotrue/config/.env ] && [ -z "$TIGRIS_SKIP_LOCAL_AUTH" ] ; then + start_gotrue + fi + + echo "Successfully started" + date + + wait $pid +} + +main 2>&1 | tee -a $D/init.log + diff --git a/scripts/test_docker_local.sh b/scripts/test_docker_local.sh new file mode 100644 index 000000000..cd971ff71 --- /dev/null +++ b/scripts/test_docker_local.sh @@ -0,0 +1,150 @@ +#!/bin/bash + +set -ex + +cli=./tigris +export cli + +CN=test-docker-local + +curl -sSL https://tigris.dev/cli-linux | tar -xz -C . + +make docker-local + +PORT=9981 +TMPDIR=/tmp/test_docker_local + +export TIGRIS_URL=localhost:$PORT +export TIGRIS_TEST_PORT=$PORT +export TIGRIS_CLI_TEST_FAST=1 +export TIGRIS_SKIP_LOCAL_TLS=true +export TIGRIS_NO_TEST_CONFIG=1 +export TIGRIS_PROTOCOL=http +export TIGRIS_LOCAL_GENERATE_ADMIN_TOKEN=1 + +wait_up() { + TIGRIS_LOG_LEVEL=debug $cli ping --timeout 30s +} + +stop() { + docker stop $CN || true + docker rm --force $CN || true +} + +test_ephemeral() { + echo -e "\nTest ephemeral instance (started)" + + stop + + docker run --name $CN -d -p $PORT:8081 tigris_local + + wait_up + + noup=1 /bin/bash test/v1/cli/main.sh + + $cli create project test_docker_local + + stop + + docker run --name $CN -d -p $PORT:8081 tigris_local + + wait_up + + $cli list projects | grep test_docker_local && exit 1 + + echo -e "Test ephemeral instance (finished)" +} + +test_persistent() { + echo -e "\nTest persistent instance (started)" + + stop + + $SUDO rm -rf $TMPDIR + mkdir -p $TMPDIR + + docker run --name $CN -e TIGRIS_LOCAL_PERSISTENCE=1 -v $TMPDIR:/var/lib/tigris -d -p $PORT:8081 tigris_local + + wait_up + + noup=1 /bin/bash test/v1/cli/main.sh + + $cli create project test_docker_local + + stop + + docker run --name $CN -v $TMPDIR:/var/lib/tigris -d -p $PORT:8081 tigris_local + + wait_up + + noup=1 /bin/bash test/v1/cli/main.sh + + $cli list projects | grep test_docker_local + + echo -e "\nTest persistent instance (finished)" +} + +test_auth() { + echo -e "\nTest persistent authenticated instance (started)" + + stop + + $SUDO rm -rf $TMPDIR + mkdir -p $TMPDIR + + docker run -e TIGRIS_BOOTSTRAP_LOCAL_AUTH=1 \ + -e TIGRIS_LOCAL_PERSISTENCE=1 \ + -e TIGRIS_LOCAL_GENERATE_ADMIN_TOKEN=1 \ + --name $CN -v $TMPDIR:/var/lib/tigris \ + -d -p $PORT:8081 tigris_local + + wait_up + + TIGRIS_TOKEN=$(cat /tmp/test_docker_local/user_admin_token.txt) + export TIGRIS_TOKEN + + wait_up # wait for gotrue + + noup=1 /bin/bash test/v1/cli/main.sh + + $cli create project test_docker_local + + stop + + # Restart the instance with auth + docker run --name $CN -v $TMPDIR:/var/lib/tigris -d -p $PORT:8081 tigris_local + + wait_up + + noup=1 /bin/bash test/v1/cli/main.sh + + $cli list projects | grep test_docker_local + + unset TIGRIS_TOKEN + + $cli list projects | grep test_docker_local && exit 1 # unauthenticated + + # container owner automatically authenticated on unix socket connection + $SUDO TIGRIS_PROTOCOL=http TIGRIS_URL=$TMPDIR/server/unix.sock $cli list projects | grep test_docker_local + $SUDO TIGRIS_PROTOCOL=grpc TIGRIS_URL=$TMPDIR/server/unix.sock $cli list projects | grep test_docker_local + + stop + + # Test unauthenticated existing instance + docker run -e TIGRIS_SKIP_LOCAL_AUTH=1 \ + --name $CN -v $TMPDIR:/var/lib/tigris -d -p $PORT:8081 tigris_local + + wait_up + + noup=1 /bin/bash test/v1/cli/main.sh + + $cli list projects | grep test_docker_local + + echo "Test persistent authenticated instance (finished)" +} + +stop + +test_ephemeral +test_persistent +test_auth diff --git a/server/config/options.go b/server/config/options.go index 8875d5908..5189dafac 100644 --- a/server/config/options.go +++ b/server/config/options.go @@ -32,6 +32,11 @@ type ServerConfig struct { Type string `json:"type" mapstructure:"type" yaml:"type"` FDBHardDrop bool `json:"fdb_hard_drop" mapstructure:"fdb_hard_drop" yaml:"fdb_hard_drop"` RealtimePort int16 `json:"realtime_port" mapstructure:"realtime_port" yaml:"realtime_port"` + UnixSocket string `json:"unix_socket" mapstructure:"unix_socket" yaml:"unix_socket"` + TLSKey string `json:"tls_key" mapstructure:"tls_key" yaml:"tls_key"` + TLSCert string `json:"tls_cert" mapstructure:"tls_cert" yaml:"tls_cert"` + // route TLS traffic to HTTP, by default GRPC handles TLS traffic + TLSHttp bool `json:"tls_http" mapstructure:"tls_http" yaml:"tls_http"` } type Config struct { @@ -319,6 +324,7 @@ var DefaultConfig = Config{ Type: DatabaseServerType, Host: "0.0.0.0", Port: 8081, + UnixSocket: "", RealtimePort: 8083, FDBHardDrop: true, }, diff --git a/server/main.go b/server/main.go index 306e710d4..3c261ecfc 100644 --- a/server/main.go +++ b/server/main.go @@ -133,11 +133,10 @@ func mainWithCode() int { if cfg.Server.Type == config.RealtimeServerType { port = cfg.Server.RealtimePort } - if err := mx.Start(cfg.Server.Host, port); err != nil { - log.Error().Err(err).Msgf("error starting realtime server") - return 1 - } + + mx.Start(cfg.Server.Host, port, cfg.Server.UnixSocket) log.Info().Msg("Shutdown") + return 0 } diff --git a/server/middleware/auth.go b/server/middleware/auth.go index 366c7c1e8..218481ccb 100644 --- a/server/middleware/auth.go +++ b/server/middleware/auth.go @@ -152,11 +152,21 @@ func measuredAuthFunction(ctx context.Context, jwtValidators []*validator.Valida return ctxResult, nil } -func authFunction(ctx context.Context, jwtValidators []*validator.Validator, config *config.Config, cache gcache.Cache, a auth.Provider) (ctxResult context.Context, err error) { +func authFunction(ctx context.Context, jwtValidators []*validator.Validator, config *config.Config, cache gcache.Cache, a auth.Provider, +) (resCtx context.Context, err error) { + if request.IsLocalRoot(ctx) { + // root and no token present + tkn, err := AuthFromMD(ctx, "bearer") + if err != nil || tkn == "" { + return ctx, nil + } + } + reqMetadata, err := request.GetRequestMetadataFromContext(ctx) if err != nil { log.Warn().Err(err).Msg("Failed to load request metadata") } + defer func() { if err != nil { if config.Auth.LogOnly { @@ -170,11 +180,13 @@ func authFunction(ctx context.Context, jwtValidators []*validator.Validator, con } } }() + // disable health check authn/z fullMethodName, fullMethodNameFound := grpc.Method(ctx) if fullMethodNameFound && BypassAuthForTheseMethods.Contains(fullMethodName) { return ctx, nil } + tkn, err := AuthFromMD(ctx, "bearer") if err != nil { return ctx, err @@ -211,13 +223,12 @@ func authenticateUsingAuthToken(ctx context.Context, jwtValidators []*validator. var err error // if not found from cache if validatedToken == nil { - count := 0 for i, jwtValidator := range jwtValidators { validatedToken, err = jwtValidator.ValidateToken(ctx, tkn) if err == nil { break } else if config.Auth.EnableErrorLog { - log.Err(err).Int("count", count).Msg("Failed to validate the token moving to next validator") + log.Err(err).Int("count", i).Msg("Failed to validate the token moving to next validator") } // if none of the validator validated the token, then reject the request if i == len(jwtValidators)-1 && err != nil { @@ -234,7 +245,6 @@ func authenticateUsingAuthToken(ctx context.Context, jwtValidators []*validator. _ = cache.Remove(tkn) return ctx, errors.Unauthenticated("Failed to validate access token, could not be validated") } - count++ } } diff --git a/server/middleware/authz.go b/server/middleware/authz.go index 65c7f9886..505c3b6aa 100644 --- a/server/middleware/authz.go +++ b/server/middleware/authz.go @@ -377,7 +377,7 @@ var ( func authzUnaryServerInterceptor() func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - if config.DefaultConfig.Auth.Authz.Enabled { + if config.DefaultConfig.Auth.Enabled && config.DefaultConfig.Auth.Authz.Enabled { err := authorize(ctx) if err != nil { return nil, err @@ -389,15 +389,21 @@ func authzUnaryServerInterceptor() func(ctx context.Context, req any, info *grpc func authzStreamServerInterceptor() grpc.StreamServerInterceptor { return func(srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - err := authorize(stream.Context()) - if err != nil { - return err + if config.DefaultConfig.Auth.Enabled && config.DefaultConfig.Auth.Authz.Enabled { + err := authorize(stream.Context()) + if err != nil { + return err + } } return handler(srv, stream) } } func authorize(ctx context.Context) (err error) { + if request.IsLocalRoot(ctx) { + return nil + } + defer func() { if err != nil { if config.DefaultConfig.Auth.Authz.LogOnly { @@ -419,6 +425,7 @@ func authorize(ctx context.Context) (err error) { if err != nil { return errors.PermissionDenied("Couldn't read the accessToken, reason: %s", err.Error()) } + role := getRole(reqMetadata) if role == "" { log.Warn(). diff --git a/server/middleware/namespace.go b/server/middleware/namespace.go index 65894bb9c..ee7c1f204 100644 --- a/server/middleware/namespace.go +++ b/server/middleware/namespace.go @@ -37,12 +37,13 @@ var ( func namespaceSetterUnaryServerInterceptor(enabled bool) func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { namespace := defaults.DefaultNamespaceName + reqMetadata, err := request.GetRequestMetadataFromContext(ctx) if err != nil { ulog.E(err) } - if enabled && !excludedMethods.Contains(info.FullMethod) { - var err error + + if enabled && !excludedMethods.Contains(info.FullMethod) && !request.IsLocalRoot(ctx) { if namespace, err = namespaceExtractor.Extract(ctx); err != nil { // We know that getAccessToken with app_id/app_secret credentials doesn't have namespace set. // Mark it as default_namespace instead of unknown. @@ -50,10 +51,13 @@ func namespaceSetterUnaryServerInterceptor(enabled bool) func(ctx context.Contex // We return and error when the token is set, but namespace is empty return nil, err } + namespace = defaults.DefaultNamespaceName } } + reqMetadata.SetNamespace(ctx, namespace) + return handler(ctx, req) } } diff --git a/server/muxer/grpc.go b/server/muxer/grpc.go index 61da590a6..b3098c9e0 100644 --- a/server/muxer/grpc.go +++ b/server/muxer/grpc.go @@ -15,11 +15,20 @@ package muxer import ( + "crypto/tls" + "io" + "net" + "os" + "github.com/rs/zerolog/log" "github.com/soheilhy/cmux" "github.com/tigrisdata/tigris/server/config" "github.com/tigrisdata/tigris/server/middleware" + "github.com/tigrisdata/tigris/server/request" + "github.com/tigrisdata/tigris/util" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/local" "google.golang.org/grpc/reflection" ) @@ -29,23 +38,117 @@ const ( type GRPCServer struct { *grpc.Server + ServeTLS bool +} + +func initTLS(cfg *config.Config) *tls.Config { + if len(cfg.Server.TLSCert) > 0 && (cfg.Server.TLSCert[0] == '/' || cfg.Server.TLSCert[0] == '.') { + cert, err := os.ReadFile(cfg.Server.TLSCert) + util.Fatal(err, "reading TLS cert from file") + + log.Info().Str("file", cfg.Server.TLSCert).Msg("read certificate from file") + + cfg.Server.TLSCert = string(cert) + } + + if len(cfg.Server.TLSKey) > 0 && (cfg.Server.TLSKey[0] == '/' || cfg.Server.TLSKey[0] == '.') { + key, err := os.ReadFile(cfg.Server.TLSKey) + util.Fatal(err, "reading TLS private key from file") + + log.Info().Str("file", cfg.Server.TLSKey).Msg("read private key from file") + + cfg.Server.TLSCert = string(key) + } + + cert, err := tls.X509KeyPair([]byte(cfg.Server.TLSCert), []byte(cfg.Server.TLSKey)) + util.Fatal(err, "loading X509 certificate") + + tlsConfig := &tls.Config{ + ServerName: "localhost", + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + } + + log.Info().Msg("initializing TLS") + + return tlsConfig +} + +type UnixPeerCredentials struct { + credentials.TransportCredentials +} + +func (*UnixPeerCredentials) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + ai := request.AuthInfo{ + CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}, + } + + c, ok := conn.(*cmux.MuxConn) + if !ok { + return conn, &ai, nil + } + + creds, err := util.ReadPeerCreds(c.Conn) + if err != nil { + return conn, &ai, nil //nolint:nilerr + } + + log.Debug().Msgf("grpc server handshake. user id=%v", creds.Uid) + + ai.LocalRoot = creds.Uid == 0 + + if ai.LocalRoot { + log.Debug().Msg("Local root user detected") + } + + ai.CommonAuthInfo.SecurityLevel = credentials.PrivacyAndIntegrity + + return conn, &ai, nil } func NewGRPCServer(cfg *config.Config) *GRPCServer { s := &GRPCServer{} unary, stream := middleware.Get(cfg) - s.Server = grpc.NewServer(grpc.StreamInterceptor(stream), grpc.UnaryInterceptor(unary), grpc.MaxRecvMsgSize(defaultTigrisServerMaxReceiveMessageSize)) + + opts := []grpc.ServerOption{ + grpc.StreamInterceptor(stream), + grpc.UnaryInterceptor(unary), + grpc.MaxRecvMsgSize(defaultTigrisServerMaxReceiveMessageSize), + } + + if cfg.Server.UnixSocket != "" { + opts = append(opts, grpc.Creds(&UnixPeerCredentials{ + local.NewCredentials(), + })) + } + + if (cfg.Server.TLSCert != "" || cfg.Server.TLSKey != "") && !cfg.Server.TLSHttp { + opts = append(opts, grpc.Creds(credentials.NewTLS(initTLS(cfg)))) + s.ServeTLS = true + } + + s.Server = grpc.NewServer(opts...) + reflection.Register(s) + return s } func (s *GRPCServer) Start(mux cmux.CMux) error { // MatchWithWriters is needed as it needs SETTINGS frame from the server otherwise the client will block - match := mux.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc")) + matchers := []cmux.MatchWriter{cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc")} + + if s.ServeTLS { + matchers = append(matchers, func(_ io.Writer, r io.Reader) bool { return cmux.TLS()(r) }) + } + + match := mux.MatchWithWriters(matchers...) + go func() { err := s.Serve(match) log.Fatal().Err(err).Msg("start http server") }() + return nil } diff --git a/server/muxer/http.go b/server/muxer/http.go index 981ea502c..f94ba3e9a 100644 --- a/server/muxer/http.go +++ b/server/muxer/http.go @@ -15,7 +15,11 @@ package muxer import ( + "context" + "crypto/tls" + "net" "net/http" + "reflect" "time" "github.com/fullstorydev/grpchan/inprocgrpc" @@ -26,19 +30,28 @@ import ( "github.com/soheilhy/cmux" "github.com/tigrisdata/tigris/server/config" "github.com/tigrisdata/tigris/server/middleware" + "github.com/tigrisdata/tigris/server/request" + "github.com/tigrisdata/tigris/util" ) const readHeaderTimeout = 5 * time.Second type HTTPServer struct { - Router chi.Router - Inproc *inprocgrpc.Channel + Router chi.Router + Inproc *inprocgrpc.Channel + tlsConfig *tls.Config + UnixSocket bool } func NewHTTPServer(cfg *config.Config) *HTTPServer { server := &HTTPServer{ - Inproc: &inprocgrpc.Channel{}, - Router: chi.NewRouter(), + Inproc: &inprocgrpc.Channel{}, + Router: chi.NewRouter(), + UnixSocket: len(cfg.Server.UnixSocket) > 0, + } + + if (cfg.Server.TLSKey != "" || cfg.Server.TLSCert != "") && cfg.Server.TLSHttp { + server.tlsConfig = initTLS(cfg) } server.SetupMiddlewares(cfg) @@ -56,6 +69,7 @@ func (s *HTTPServer) SetupMiddlewares(cfg *config.Config) { s.Inproc.WithServerUnaryInterceptor(unary) s.Router.Use(cors.AllowAll().Handler) + if cfg.Server.Type == config.RealtimeServerType { s.Router.Use(middleware.HTTPMetadataExtractorMiddleware(cfg)) s.Router.Use(middleware.HTTPAuthMiddleware(cfg)) @@ -63,11 +77,57 @@ func (s *HTTPServer) SetupMiddlewares(cfg *config.Config) { } func (s *HTTPServer) Start(mux cmux.CMux) error { - match := mux.Match(cmux.HTTP1Fast("PATCH"), cmux.HTTP1HeaderField("Upgrade", "websocket")) + matchers := []cmux.Matcher{cmux.HTTP1Fast("PATCH"), cmux.HTTP1HeaderField("Upgrade", "websocket")} + if s.tlsConfig != nil { + matchers = append(matchers, cmux.TLS()) + } + + match := mux.Match(matchers...) + go func() { - srv := &http.Server{Handler: s.Router, ReadHeaderTimeout: readHeaderTimeout} - err := srv.Serve(match) + srv := &http.Server{ + Handler: s.Router, + ReadHeaderTimeout: readHeaderTimeout, + TLSConfig: s.tlsConfig, + } + + if s.UnixSocket { + srv.ConnContext = func(ctx context.Context, conn net.Conn) context.Context { + var nc net.Conn + + c, ok := conn.(*cmux.MuxConn) + if !ok { + tl, ok := conn.(*tls.Conn) + if !ok { + log.Debug().Msgf("not cmux connection: %v", reflect.TypeOf(conn)) + return ctx + } + + nc = tl.NetConn() + } else { + nc = c.Conn + } + + creds, err := util.ReadPeerCreds(nc) + if err == nil && creds.Uid == 0 { + log.Debug().Msgf("local root on http") + return request.SetLocalRoot(ctx) + } + + return ctx + } + } + + var err error + + if srv.TLSConfig != nil { + err = srv.ServeTLS(match, "", "") + } else { + err = srv.Serve(match) + } + log.Fatal().Err(err).Msg("start http server") }() + return nil } diff --git a/server/muxer/muxer.go b/server/muxer/muxer.go index 87573f8e3..04f5a620e 100644 --- a/server/muxer/muxer.go +++ b/server/muxer/muxer.go @@ -17,6 +17,8 @@ package muxer import ( "fmt" "net" + "os" + "runtime" "github.com/rs/zerolog/log" "github.com/soheilhy/cmux" @@ -27,6 +29,7 @@ import ( "github.com/tigrisdata/tigris/server/transaction" "github.com/tigrisdata/tigris/store/kv" "github.com/tigrisdata/tigris/store/search" + "github.com/tigrisdata/tigris/util" ulog "github.com/tigrisdata/tigris/util/log" ) @@ -64,18 +67,47 @@ func (m *Muxer) RegisterServices(cfg *config.ServerConfig, kvStore kv.TxStore, s } } -func (m *Muxer) Start(host string, port int16) error { - log.Info().Int16("port", port).Msg("initializing server") - - l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port)) - if err != nil { - log.Fatal().Err(err).Msg("listening failed ") - } - +func (m *Muxer) serve(l net.Listener) { cm := cmux.New(l) + for _, s := range m.servers { _ = s.Start(cm) } - log.Info().Msg("server started, servicing requests") - return cm.Serve() + + err := cm.Serve() + util.Fatal(err, "serve") +} + +func (m *Muxer) Start(host string, port int16, unix string) { + if unix == "" && port == 0 { + util.Fatal(fmt.Errorf("please configure TCP host:port or unix socket for server to bind to"), "binding ports") + } + + if unix != "" && (runtime.GOOS != "windows") { + log.Info().Str("uds", unix).Msg("initializing server") + + ulog.E(os.Remove(unix)) + + l, err := net.Listen("unix", unix) + util.Fatal(err, "listen on unix domain socket") + + if port != 0 { + go m.serve(l) + } else { + m.serve(l) + } + } + + if port == 0 && (runtime.GOOS == "windows") { + port = 8081 + } + + if port != 0 { + log.Info().Str("host", host).Int16("port", port).Msg("initializing server") + + l, err := net.Listen("tcp", net.JoinHostPort(host, fmt.Sprintf("%d", port))) + util.Fatal(err, "listen on tcp port") + + m.serve(l) + } } diff --git a/server/request/request.go b/server/request/request.go index 0ac480755..ecea18cb5 100644 --- a/server/request/request.go +++ b/server/request/request.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/buger/jsonparser" + "github.com/fullstorydev/grpchan/inprocgrpc" "github.com/rs/zerolog/log" api "github.com/tigrisdata/tigris/api/server/v1" "github.com/tigrisdata/tigris/errors" @@ -32,6 +33,8 @@ import ( "github.com/tigrisdata/tigris/server/types" ulog "github.com/tigrisdata/tigris/util/log" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" ) const ( @@ -51,7 +54,10 @@ var ( tenantGetter metadata.TenantGetter ) -type MetadataCtxKey struct{} +type ( + MetadataCtxKey struct{} + localRootCtxKey struct{} +) type Metadata struct { accessToken *types.AccessToken @@ -289,6 +295,9 @@ func GetAccessToken(ctx context.Context) (*types.AccessToken, error) { func GetCurrentSub(ctx context.Context) (string, error) { tkn, err := GetAccessToken(ctx) if err != nil { + if IsLocalRoot(ctx) { + return "root", nil + } return "", err } return tkn.Sub, nil @@ -456,3 +465,45 @@ func IsAcceptApplicationJSON(ctx context.Context) bool { // we need to only check non grpc gateway prefix return api.GetNonGRPCGatewayHeader(ctx, api.HeaderAccept) == AcceptTypeApplicationJSON } + +// AuthInfo is used for root authentication on the unix socket peer. +type AuthInfo struct { + credentials.CommonAuthInfo + LocalRoot bool +} + +// AuthType returns the type of info as a string. +func (AuthInfo) AuthType() string { + return "local_root" +} + +func SetLocalRoot(ctx context.Context) context.Context { + return context.WithValue(ctx, localRootCtxKey{}, true) +} + +func IsLocalRoot(ctx context.Context) bool { + if pc, ok := peer.FromContext(ctx); ok { + if ai, ok := pc.AuthInfo.(*AuthInfo); ok { + log.Debug().Msg("local root detected from grpc") + return ai.LocalRoot + } + } + + var val any + + oCtx := inprocgrpc.ClientContext(ctx) + if oCtx != nil { + val = oCtx.Value(localRootCtxKey{}) + } else { + val = ctx.Value(localRootCtxKey{}) + } + + if val != nil { + if b, ok := val.(bool); ok && b { + log.Debug().Msg("local root detected from http") + return true + } + } + + return false +} diff --git a/server/services/v1/auth/common.go b/server/services/v1/auth/common.go index 586b437cf..3e7d55b54 100644 --- a/server/services/v1/auth/common.go +++ b/server/services/v1/auth/common.go @@ -128,6 +128,9 @@ func GetCurrentSub(ctx context.Context) (string, error) { // further filter for this particular user token, err := request.GetAccessToken(ctx) if err != nil { + if request.IsLocalRoot(ctx) { + return "root", nil + } return "", errors.Internal("Failed to retrieve current sub: reason = %s", err.Error()) } return token.Sub, nil diff --git a/server/services/v1/auth/gotrue.go b/server/services/v1/auth/gotrue.go index c1a94c1b4..f5616f800 100644 --- a/server/services/v1/auth/gotrue.go +++ b/server/services/v1/auth/gotrue.go @@ -832,6 +832,7 @@ func getAccessTokenUsingClientCredentialsGotrue(ctx context.Context, clientId st payloadValues := url.Values{} payloadValues.Set("username", clientId) payloadValues.Set("password", clientSecret) + client := &http.Client{} getTokenReq, err := http.NewRequestWithContext(ctx, http.MethodPost, getTokenUrl, strings.NewReader(payloadValues.Encode())) diff --git a/server/services/v1/database/database_runner.go b/server/services/v1/database/database_runner.go index 184fb8424..1b67e9546 100644 --- a/server/services/v1/database/database_runner.go +++ b/server/services/v1/database/database_runner.go @@ -88,6 +88,7 @@ func (runner *ProjectQueryRunner) delete(ctx context.Context, tx transaction.Tx, func (*ProjectQueryRunner) list(ctx context.Context, _ transaction.Tx, tenant *metadata.Tenant) (Response, context.Context, error) { // listReq projects need not include any branches + projectList := tenant.ListProjects(ctx) projects := make([]*api.ProjectInfo, len(projectList)) for i, l := range projectList { diff --git a/store/kv/fdb.go b/store/kv/fdb.go index ea4f06fb2..50542ed37 100644 --- a/store/kv/fdb.go +++ b/store/kv/fdb.go @@ -496,7 +496,7 @@ func (t *ftx) Commit(_ context.Context) error { func (t *ftx) Rollback(_ context.Context) error { t.tx.Cancel() - log.Debug().Msg("tx Rollback") + log.Trace().Msg("tx Rollback") return nil } diff --git a/test/config/config.go b/test/config/config.go index 56b54d60c..195970a1d 100644 --- a/test/config/config.go +++ b/test/config/config.go @@ -45,6 +45,16 @@ func GetBaseURL2() string { return "http://localhost:8082" } +func GetBaseURL2Auth() string { + config.LoadEnvironment() + + if config.GetEnvironment() == config.EnvTest { + return "https://tigris_server2:8081" + } + + return "https://localhost:8082" +} + func GetGotrueURL() string { config.LoadEnvironment() diff --git a/test/docker/test-token-A-hs.jwt b/test/config/keys/test-token-A-hs.jwt similarity index 100% rename from test/docker/test-token-A-hs.jwt rename to test/config/keys/test-token-A-hs.jwt diff --git a/test/docker/test-token-B-hs.jwt b/test/config/keys/test-token-B-hs.jwt similarity index 100% rename from test/docker/test-token-B-hs.jwt rename to test/config/keys/test-token-B-hs.jwt diff --git a/test/docker/test-token-hs.jwt b/test/config/keys/test-token-hs.jwt similarity index 100% rename from test/docker/test-token-hs.jwt rename to test/config/keys/test-token-hs.jwt diff --git a/test/docker/test-token-rsa-admin.jwt b/test/config/keys/test-token-rsa-admin.jwt similarity index 100% rename from test/docker/test-token-rsa-admin.jwt rename to test/config/keys/test-token-rsa-admin.jwt diff --git a/test/docker/test-token-rsa-editor.jwt b/test/config/keys/test-token-rsa-editor.jwt similarity index 100% rename from test/docker/test-token-rsa-editor.jwt rename to test/config/keys/test-token-rsa-editor.jwt diff --git a/test/docker/test-token-rsa-owner.jwt b/test/config/keys/test-token-rsa-owner.jwt similarity index 100% rename from test/docker/test-token-rsa-owner.jwt rename to test/config/keys/test-token-rsa-owner.jwt diff --git a/test/docker/test-token-rsa-readonly.jwt b/test/config/keys/test-token-rsa-readonly.jwt similarity index 100% rename from test/docker/test-token-rsa-readonly.jwt rename to test/config/keys/test-token-rsa-readonly.jwt diff --git a/test/docker/test-token-rsa.jwt b/test/config/keys/test-token-rsa.jwt similarity index 100% rename from test/docker/test-token-rsa.jwt rename to test/config/keys/test-token-rsa.jwt diff --git a/test/config/keys/test_ca.crt b/test/config/keys/test_ca.crt new file mode 100644 index 000000000..cbd5ee477 --- /dev/null +++ b/test/config/keys/test_ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDDjCCAnegAwIBAgIUcFYwOVrBF3AMmdMbBuUK/l+HOhkwDQYJKoZIhvcNAQEL +BQAwgYUxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNTW91bnRh +aW4gVmlldzEQMA4GA1UECgwHVGVzdE9yZzEMMAoGA1UECwwDRGV2MRIwEAYDVQQD +DAlsb2NhbGhvc3QxHTAbBgkqhkiG9w0BCQEWDnJvb3RAbG9jYWxob3N0MB4XDTIz +MDUzMDIxMDkzMloXDTMzMDUyNzIxMDkzMlowgYUxCzAJBgNVBAYTAlVTMQswCQYD +VQQIDAJDQTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEQMA4GA1UECgwHVGVzdE9y +ZzEMMAoGA1UECwwDRGV2MRIwEAYDVQQDDAlsb2NhbGhvc3QxHTAbBgkqhkiG9w0B +CQEWDnJvb3RAbG9jYWxob3N0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCw +6jq/HFAnc5btMWnEsc9y13Xpa9VvXs1Yo8kN08VV4b7n2wgwcPpsSO3d1o/CaijT +Wr1O64XH+jn4WhEWwoVG8hp4nwup+pdYbPqJftK2ndEo5/cIT2F4gsT8Y8DunuaL +IKUFYZSgEKqymFSTeC1tTyN5izxKTTmc00stxQNbIwIDAQABo3kwdzAdBgNVHQ4E +FgQUdYepMzctHS8b/O33YPHAcfccM/owHwYDVR0jBBgwFoAUdYepMzctHS8b/O33 +YPHAcfccM/owDwYDVR0TAQH/BAUwAwEB/zAkBgNVHREEHTAbgglsb2NhbGhvc3SC +DnRpZ3Jpc19zZXJ2ZXIyMA0GCSqGSIb3DQEBCwUAA4GBAER8YzJUk7hb6UnR/NIo +wiKL1qMm3YMHuHSJ8ZNka4sbTUXS5vqLdqGUnZBiJH/+bIBXJWZIRR3VZLOcFvhq +qCVW5kb0kI6TNwN3pFtb43Kt8JO/Odid4Lv3EMWvIGq2zwuBfzCRRZUcZABf89SQ +T8K2McQFzBPrsEr9Q16NfCOO +-----END CERTIFICATE----- diff --git a/config/server.dev.yaml b/test/config/server.dev.yaml similarity index 96% rename from config/server.dev.yaml rename to test/config/server.dev.yaml index 06b43acde..1dd8f2ac6 100644 --- a/config/server.dev.yaml +++ b/test/config/server.dev.yaml @@ -17,8 +17,7 @@ server: host: localhost port: 8081 - chunking: true - compression: true + unix_socket: /tmp/tigris_test_server.sock cdc: enabled: false diff --git a/config/server.test.yaml b/test/config/server.test.yaml similarity index 50% rename from config/server.test.yaml rename to test/config/server.test.yaml index ccc88dd66..9a0a57b91 100644 --- a/config/server.test.yaml +++ b/test/config/server.test.yaml @@ -17,6 +17,9 @@ server: port: 8081 +cache: + host: tigris_cache + environment: test search: @@ -24,6 +27,7 @@ search: host: tigris_search chunking: true compression: true + read_enabled: true kv: chunking: true @@ -40,41 +44,11 @@ secondary_index: mutate_enabled: true auth: + enabled: false + +tracing: + enabled: true + +workers: enabled: true - authz: - enabled: true - log_only: false - enable_namespace_isolation: true - enable_oauth: true - log_only: false - admin_namespaces: - - tigris_test - validators: - - issuer: http://tigris_gotrue:8086 - algorithm: RS256 - audience: https://tigris-test - - issuer: http://tigris_gotrue:8086 - algorithm: HS256 - audience: https://tigris-test - - issuer: http://tigris_gotrue:8086 - algorithm: HS256 - audience: https://tigris-testA - - issuer: http://tigris_gotrue:8086 - algorithm: HS256 - audience: https://tigris-testB - api_keys: - auds: - - https://tigris-test - length: 120 - email_suffix: "@apikey.tigrisdata.com" - user_password: hello - token_cache_size: 100 - primary_audience: https://tigris-test - oauth_provider: gotrue - user_invitations: - expire_after_sec: 120 - gotrue: - username_suffix: "@m2m.tigrisdata.com" - url: http://tigris_gotrue:8086 - admin_username: test - admin_password: test + search_enabled: true diff --git a/test/config/server.test2.yaml b/test/config/server.test2.yaml new file mode 100644 index 000000000..b5c5c92a1 --- /dev/null +++ b/test/config/server.test2.yaml @@ -0,0 +1,119 @@ +# Copyright 2022-2023 Tigris Data, 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. + +# This config file is used by tigris_server2 to run auth tests + +server: + port: 8081 + tls_http: true + tls_key: | + -----BEGIN PRIVATE KEY----- + MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALiG9I8q2byHtsYs + swrhGDgVfaFi9xr1WdERyVAgrz/+F/5gwTTzsIjjIL0a1gcfvOKi7+5LCG+z36yy + pXUPd2iEfWuPH5Fw2oMRMYcqzkaAnJU7ANFeuoyHIqxx5uALuYKGK9r0dJGfTObG + 3k/NabtUK6FC6Mfm58aGn3Nj7EylAgMBAAECgYAKbdY0oT9dIG58FNqpqr8rrEtF + a8p7g5Jn9pFiLfa5ryq7/cvtqjg4BF49Ud722Bxc0sistyDq70edAxvG2fDtpr0T + v46CrxYmoxPk9CLl1oD+CRttb5ZQz8amTFM3tbrt80/5I2eVLmL6dUFCFzOM34bj + 0K7pkHR1hqU5eA5ioQJBANza7PJoV7H8smLezGc/ebUk9eZWl1YR719lKpjIdZed + xJXKMuDI12kMoasObmKslr8Vavdvc4rbU8UYZxVb8EMCQQDV5B6jMdFpG1idAgBt + 3bsBLlLiHDpg3aqOnq1Sp9Af4fv8sGp4QGvWlenchdXGIgt9DSJrWsOKE8aVqDcR + wNT3AkAyA12vE1PwmXHoE94j72rnS4Rn8en5crxLVQSNbq+6ct7GsPBOmQy23EZs + DyuOKtlEUlxTxihbJInW00zcuGIzAkEAnYY18iacfXZAWtHAkyl7sjD1pcT4UaKv + G/5M09T3eKOsO7uJjiqTwSQDaf+/Iv6ry1tDACGGZUiPNmT+ubp0nQJAdbjUr5g7 + YcFh8uAwMSDzHhDU5vv2eZPIRWLixrV06vjX5HPgdpSTJCV9dN5s3HqveGVfpyLa + 9ggFNQi5LKAogA== + -----END PRIVATE KEY----- + tls_cert: | + -----BEGIN CERTIFICATE----- + MIIDAjCCAmugAwIBAgIUCou8ijCU05zOwyc44m46PeMio+0wDQYJKoZIhvcNAQEL + BQAwgYUxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNTW91bnRh + aW4gVmlldzEQMA4GA1UECgwHVGVzdE9yZzEMMAoGA1UECwwDRGV2MRIwEAYDVQQD + DAlsb2NhbGhvc3QxHTAbBgkqhkiG9w0BCQEWDnJvb3RAbG9jYWxob3N0MB4XDTIz + MDUzMDIxMDkzMloXDTMzMDUyNzIxMDkzMloweDELMAkGA1UEBhMCVVMxCzAJBgNV + BAgMAkNBMQswCQYDVQQHDAJNVjELMAkGA1UECgwCUEMxDzANBgNVBAsMBkxhcHRv + cDESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZIhvcNAQkBFg5yb290QGxvY2Fs + aG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuIb0jyrZvIe2xiyzCuEY + OBV9oWL3GvVZ0RHJUCCvP/4X/mDBNPOwiOMgvRrWBx+84qLv7ksIb7PfrLKldQ93 + aIR9a48fkXDagxExhyrORoCclTsA0V66jIcirHHm4Au5goYr2vR0kZ9M5sbeT81p + u1QroULox+bnxoafc2PsTKUCAwEAAaN7MHkwNwYDVR0RBDAwLoIJbG9jYWxob3N0 + ggsqLmxvY2FsaG9zdIIOdGlncmlzX3NlcnZlcjKHBH8AAAEwHQYDVR0OBBYEFMXJ + lCC/y0ZzRGdJy/Ffg1U52/BsMB8GA1UdIwQYMBaAFHWHqTM3LR0vG/zt92DxwHH3 + HDP6MA0GCSqGSIb3DQEBCwUAA4GBAAhIDMQl8sqItEeUaogTEgX4musE8BqxUx83 + 5qKTvs0q2g1qDUGOkkG6FnL3c5yXELahjkWnKJzVeesbq+jd1Xg9By3KkJVXxbos + NmdUo1LACDSJYyI2kzKbj25IBGBqxky1EAyUKaG9aUO/rOznJKwg2gN8BbayMwwC + q0HcfOVW + -----END CERTIFICATE----- + +environment: test + +search: + auth_key: ts_test_key + host: tigris_search + chunking: true + compression: true + +kv: + chunking: true + compression: true + min_compression_threshold: 1 + +log: + level: debug + format: console + +secondary_index: + write_enabled: true + read_enabled: true + mutate_enabled: true + +auth: + enabled: true + authz: + enabled: true + log_only: false + enable_namespace_isolation: true + enable_oauth: true + log_only: false + admin_namespaces: + - tigris_test + validators: + - issuer: http://tigris_gotrue:8086 + algorithm: RS256 + audience: https://tigris-test + - issuer: http://tigris_gotrue:8086 + algorithm: HS256 + audience: https://tigris-test + - issuer: http://tigris_gotrue:8086 + algorithm: HS256 + audience: https://tigris-testA + - issuer: http://tigris_gotrue:8086 + algorithm: HS256 + audience: https://tigris-testB + api_keys: + auds: + - https://tigris-test + length: 120 + email_suffix: "@apikey.tigrisdata.com" + user_password: hello + token_cache_size: 100 + primary_audience: https://tigris-test + oauth_provider: gotrue + user_invitations: + expire_after_sec: 120 + gotrue: + username_suffix: "@m2m.tigrisdata.com" + url: http://tigris_gotrue:8086 + admin_username: test + admin_password: test + shared_secret: test_shared_secret diff --git a/test/docker/docker-compose.yml b/test/docker/docker-compose.yml index 68358bffb..a9f0de208 100644 --- a/test/docker/docker-compose.yml +++ b/test/docker/docker-compose.yml @@ -54,25 +54,15 @@ services: image: tigris_server environment: - TIGRIS_ENVIRONMENT=test - - TIGRIS_SERVER_CACHE_HOST=tigris_cache - - TIGRIS_SERVER_LOG_FORMAT=console - - TIGRIS_AUTH_ENABLED=false - - TIGRIS_SERVER_SEARCH_AUTH_KEY=ts_test_key - - TIGRIS_SERVER_SEARCH_HOST=tigris_search - - TIGRIS_SERVER_TRACING_ENABLED=true - - TIGRIS_SERVER_SEARCH_READ_ENABLED=true - - TIGRIS_SERVER_SECONDARY_INDEX_WRITE_ENABLED=true - - TIGRIS_SERVER_SECONDARY_INDEX_READ_ENABLED=true - - TIGRIS_SERVER_SECONDARY_INDEX_MUTATE_ENABLED=true - - TIGRIS_SERVER_WORKERS_ENABLED=true - - TIGRIS_SERVER_WORKERS_SEARCH_ENABLED=true - - TIGRIS_SERVER_KV_CHUNKING=true + - TIGRIS_CONFIG_FILE=/etc/tigrisdata/tigris/server.test.yaml - GOCOVERDIR=/tmp/tigris_coverdata build: context: ../../ dockerfile: docker/Dockerfile args: - BUILD_PARAM_ARG="-cover" + - BUILD_PROFILE=.test + - CONF_PATH=test/ volumes: - type: volume source: dbdata @@ -94,15 +84,15 @@ services: container_name: tigris_server2 image: tigris_server2 environment: - - TIGRIS_CONFIG_FILE=/etc/tigrisdata/tigris/server.test.yaml - - TIGRIS_SERVER_AUTH_GOTRUE_SHARED_SECRET=test_shared_secret + - TIGRIS_CONFIG_FILE=/etc/tigrisdata/tigris/server.test2.yaml - GOCOVERDIR=/tmp/tigris_coverdata build: context: ../../ dockerfile: docker/Dockerfile args: - BUILD_PARAM_ARG="-cover" - - BUILD_PROFILE=.test + - BUILD_PROFILE=.test2 + - CONF_PATH=test/ volumes: - type: volume source: dbdata @@ -123,7 +113,7 @@ services: tigris_realtime: container_name: tigris_realtime - image: tigris_server + image: tigris_realtime environment: - TIGRIS_SERVER_SERVER_TYPE=realtime - TIGRIS_ENVIRONMENT=test diff --git a/test/v1/cli/main.sh b/test/v1/cli/main.sh index a596ee76f..a03d6bc59 100644 --- a/test/v1/cli/main.sh +++ b/test/v1/cli/main.sh @@ -24,6 +24,7 @@ if [ -z "$TIGRIS_TEST_PORT" ]; then TIGRIS_TEST_PORT=8090 fi +if [ -z "$TIGRIS_NO_TEST_CONFIG" ]; then unset TIGRIS_URL unset TIGRIS_TOKEN unset TIGRIS_CLIENT_SECRET @@ -51,6 +52,8 @@ EOF exit 1 fi +fi + #shellcheck disable=SC2154 if [ -z "$noup" ]; then TIGRIS_LOG_LEVEL=debug $cli local up "$TIGRIS_TEST_PORT" @@ -61,7 +64,7 @@ fi OS=$(uname -s) export TIGRIS_URL="localhost:$TIGRIS_TEST_PORT" -$cli server info +TIGRIS_LOG_LEVEL=debug $cli server info $cli server version if [ "$OS" == 'Darwin' ]; then @@ -93,7 +96,7 @@ db_tests() { echo "=== Test ===" echo "Proto: $TIGRIS_PROTOCOL, URL: $TIGRIS_URL" echo "============" - $cli ping + TIGRIS_LOG_LEVEL=debug $cli ping $cli delete-project -f db1 || true @@ -430,29 +433,35 @@ source "$BASEDIR/scaffold.sh" source "$BASEDIR/search/import.sh" main() { - test_config - - # Exercise tests via HTTP - unset TIGRIS_PROTOCOL - export TIGRIS_URL="localhost:$TIGRIS_TEST_PORT" - db_tests - test_import - - test_search_import - test_backup - - if [ -z "$TIGRIS_CLI_TEST_FAST" ]; then - test_scaffold + if [ -z "$TIGRIS_NO_TEST_CONFIG" ]; then + test_config fi - # Exercise tests via GRPC - export TIGRIS_URL="localhost:$TIGRIS_TEST_PORT" - export TIGRIS_PROTOCOL=grpc - $cli config show | grep "protocol: grpc" - $cli config show | grep "url: localhost:$TIGRIS_TEST_PORT" - db_tests - test_import - test_backup + if [ -z "$TIGRIS_TOKEN" ]; then + # Exercise tests via GRPC + unset TIGRIS_PROTOCOL + export TIGRIS_URL="localhost:$TIGRIS_TEST_PORT" + $cli config show + db_tests + test_import + + test_search_import + test_backup + + if [ -z "$TIGRIS_CLI_TEST_FAST" ]; then + test_scaffold + fi + + # Exercise tests via GRPC + export TIGRIS_URL="localhost:$TIGRIS_TEST_PORT" + export TIGRIS_PROTOCOL=grpc + $cli config show | grep "protocol: grpc" + $cli config show | grep "url: localhost:$TIGRIS_TEST_PORT" + db_tests + + test_import + test_backup + fi export TIGRIS_URL="localhost:$TIGRIS_TEST_PORT" export TIGRIS_PROTOCOL=http @@ -466,11 +475,13 @@ main() { $cli config show | grep "url: http://localhost:$TIGRIS_TEST_PORT" db_tests - export TIGRIS_PROTOCOL=http - export TIGRIS_URL="grpc://localhost:$TIGRIS_TEST_PORT" - $cli config show | grep "protocol: http" - $cli config show | grep "url: grpc://localhost:$TIGRIS_TEST_PORT" - db_tests + if [ -z "$TIGRIS_TOKEN" ]; then + export TIGRIS_PROTOCOL=http + export TIGRIS_URL="grpc://localhost:$TIGRIS_TEST_PORT" + $cli config show | grep "protocol: http" + $cli config show | grep "url: grpc://localhost:$TIGRIS_TEST_PORT" + db_tests + fi } main diff --git a/test/v1/server/auth_test.go b/test/v1/server/auth_test.go index aca87735f..ef63b92bf 100644 --- a/test/v1/server/auth_test.go +++ b/test/v1/server/auth_test.go @@ -17,6 +17,8 @@ package server import ( + "crypto/tls" + "crypto/x509" "fmt" "net/http" "os" @@ -34,23 +36,62 @@ import ( const ( Authorization = "Authorization" Bearer = "bearer " - RSATokenFilePath = "../../docker/test-token-rsa.jwt" //nolint:gosec - HSTokenFilePath = "../../docker/test-token-hs.jwt" //nolint:gosec - HSTokenAFilePath = "../../docker/test-token-A-hs.jwt" //nolint:gosec - HSTokenBFilePath = "../../docker/test-token-B-hs.jwt" //nolint:gosec - OwnerTokenFilePath = "../../docker/test-token-rsa-owner.jwt" //nolint:gosec - EditorTokenFilePath = "../../docker/test-token-rsa-editor.jwt" //nolint:gosec - ReadOnlyTokenFilePath = "../../docker/test-token-rsa-readonly.jwt" //nolint:gosec + keysDir = "../../config/keys" + RSATokenFilePath = keysDir + "/test-token-rsa.jwt" //nolint:gosec // test only token + HSTokenFilePath = keysDir + "/test-token-hs.jwt" //nolint:gosec // test only token + HSTokenAFilePath = keysDir + "/test-token-A-hs.jwt" //nolint:gosec // test only token + HSTokenBFilePath = keysDir + "/test-token-B-hs.jwt" //nolint:gosec // test only token + OwnerTokenFilePath = keysDir + "/test-token-rsa-owner.jwt" //nolint:gosec // test only token + EditorTokenFilePath = keysDir + "/test-token-rsa-editor.jwt" //nolint:gosec // test only token + ReadOnlyTokenFilePath = keysDir + "/test-token-rsa-readonly.jwt" //nolint:gosec // test only token + + CaCertFilePath = keysDir + "/test_ca.crt" //nolint:gosec // test only token ) +func SetupTLS(t *testing.T) *tls.Config { + t.Helper() + + cert, err := os.ReadFile(CaCertFilePath) + require.NoError(t, err) + + certPool := x509.NewCertPool() + require.True(t, certPool.AppendCertsFromPEM(cert)) + + tlsCfg := &tls.Config{RootCAs: certPool, ServerName: "localhost", MinVersion: tls.VersionTLS12} + /* + if config2.GetEnvironment() == config2.EnvTest { + tlsCfg.ServerName = "tigris_server2" + } + */ + + return tlsCfg +} + +func expectAuthLow(t *testing.T) *httpexpect.Expect { + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: SetupTLS(t), + }, + } + + e := httpexpect.WithConfig(httpexpect.Config{ + BaseURL: config.GetBaseURL2Auth(), + Reporter: httpexpect.NewRequireReporter(t), + Client: client, + }) + + return e +} + func readToken(t *testing.T, file string) string { tokenBytes, err := os.ReadFile(file) require.NoError(t, err) + return string(tokenBytes) } func createNamespaceWithToken(t *testing.T, name string, token string) *httpexpect.Response { - e := expectLow(t, config.GetBaseURL2()) + e := expectAuthLow(t) return e.POST(getCreateNamespaceURL()). WithHeader(Authorization, Bearer+token). WithJSON(Map{"name": name}). @@ -58,7 +99,7 @@ func createNamespaceWithToken(t *testing.T, name string, token string) *httpexpe } func createTestNamespace(t *testing.T, token string) { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) createNamespacePayload := Map{ "name": "tigris_test_name", "id": "tigris_test", @@ -103,7 +144,7 @@ func TestMultipleAudienceSupport(t *testing.T) { } func TestGoTrueAuthProvider(t *testing.T) { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) token := readToken(t, RSATokenFilePath) createTestNamespace(t, token) @@ -178,7 +219,8 @@ func TestGoTrueAuthProvider(t *testing.T) { require.Equal(t, id.Raw(), retrievedAppKey.Object().Value("id").String().Raw()) require.NotNil(t, retrievedAppKey.Object().Value("secret").String().Raw()) require.Equal(t, name.Raw(), retrievedAppKey.Object().Value("name").String().Raw()) - require.Equal(t, "[updated]This key is used for integration test purpose.", retrievedAppKey.Object().Value("description").String().Raw()) + require.Equal(t, "[updated]This key is used for integration test purpose.", + retrievedAppKey.Object().Value("description").String().Raw()) require.NotNil(t, retrievedAppKey.Object().Value("project").String().Raw()) require.True(t, strings.HasPrefix(retrievedAppKey.Object().Value("created_by").String().Raw(), "gt|")) @@ -228,7 +270,7 @@ func cleanupAppKeys(e *httpexpect.Expect, token string, project string) { } func TestGlobalAppKeys(t *testing.T) { - e := expectLow(t, config.GetBaseURL2()) + e := expectAuthLow(t) token := readToken(t, RSATokenFilePath) createTestNamespace(t, token) @@ -301,7 +343,8 @@ func TestGlobalAppKeys(t *testing.T) { require.Equal(t, id.Raw(), retrievedAppKey.Object().Value("id").String().Raw()) require.NotNil(t, retrievedAppKey.Object().Value("secret").String().Raw()) require.Equal(t, name.Raw(), retrievedAppKey.Object().Value("name").String().Raw()) - require.Equal(t, "[updated]This key is used for integration test purpose.", retrievedAppKey.Object().Value("description").String().Raw()) + require.Equal(t, "[updated]This key is used for integration test purpose.", + retrievedAppKey.Object().Value("description").String().Raw()) require.True(t, strings.HasPrefix(retrievedAppKey.Object().Value("created_by").String().Raw(), "gt|")) // delete @@ -313,7 +356,7 @@ func TestGlobalAppKeys(t *testing.T) { } func TestGlobalAndLocalAppKeys(t *testing.T) { - e := expectLow(t, config.GetBaseURL2()) + e := expectAuthLow(t) token := readToken(t, RSATokenFilePath) proj := "TestGlobalAndLocalAppKeys" @@ -409,10 +452,11 @@ func createGlobalAppKey(e *httpexpect.Expect, token string, name string) *httpex JSON(). Object().Value("created_app_key") } + func TestMultipleAppsCreation(t *testing.T) { testStartTime := time.Now() - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) testProject := "TestMultipleAppsCreation2" token := readToken(t, RSATokenFilePath) createTestNamespace(t, token) @@ -436,6 +480,7 @@ func TestMultipleAppsCreation(t *testing.T) { JSON(). Object().Value("app_keys").Array() require.Equal(t, 5, int(appKeys.Length().Raw())) + for _, value := range appKeys.Iter() { createdAt := int64(value.Object().Value("created_at").Number().Raw()) require.True(t, createdAt >= testStartTime.UnixMilli()) @@ -443,7 +488,7 @@ func TestMultipleAppsCreation(t *testing.T) { } func TestListAppKeys(t *testing.T) { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) testProject := "auth_test" token := readToken(t, RSATokenFilePath) createTestNamespace(t, token) @@ -481,7 +526,7 @@ func TestListAppKeys(t *testing.T) { } func TestEmptyListAppKeys(t *testing.T) { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) testProject := "TestEmptyListAppKeys" token := readToken(t, RSATokenFilePath) createTestNamespace(t, token) @@ -500,7 +545,7 @@ func TestEmptyListAppKeys(t *testing.T) { func TestApiKeyUsage(t *testing.T) { token := readToken(t, RSATokenFilePath) createTestNamespace(t, token) - e := expectLow(t, config.GetBaseURL2()) + e := expectAuthLow(t) testProject := "TestApiKey" createdApiKey := createAppKey(e, token, "test_api_key", testProject, auth.AppKeyTypeApiKey) @@ -516,7 +561,7 @@ func TestApiKeyUsage(t *testing.T) { func TestApiKeyCrud(t *testing.T) { token := readToken(t, RSATokenFilePath) createTestNamespace(t, token) - e := expectLow(t, config.GetBaseURL2()) + e := expectAuthLow(t) testProject := "TestApiKeyCrud" // create @@ -597,7 +642,7 @@ func TestApiKeyCrud(t *testing.T) { func TestWhoAmI(t *testing.T) { token := readToken(t, RSATokenFilePath) createTestNamespace(t, token) - e := expectLow(t, config.GetBaseURL2()) + e := expectAuthLow(t) res := e.GET("/v1/observability/whoami"). WithHeader(Authorization, Bearer+token). @@ -613,7 +658,7 @@ func TestWhoAmI(t *testing.T) { } func TestCreateAccessToken(t *testing.T) { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) testProject := "auth_test" token := readToken(t, RSATokenFilePath) createTestNamespace(t, token) @@ -652,7 +697,7 @@ func TestCreateAccessToken(t *testing.T) { } func TestCreateGlobalAccessToken(t *testing.T) { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) token := readToken(t, RSATokenFilePath) createTestNamespace(t, token) @@ -699,7 +744,7 @@ func TestCreateGlobalAccessToken(t *testing.T) { } func TestCreateAccessTokenUsingInvalidCreds(t *testing.T) { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) getAccessTokenResponse := e2.POST(getAuthToken()). WithFormField("client_id", "invalid-id"). WithFormField("client_secret", "invalid-password"). @@ -872,7 +917,9 @@ func TestAuthzEditor(t *testing.T) { // creating namespace is not be allowed resp := createNamespaceWithToken(t, "TestAuthzEditor", token).Status(http.StatusForbidden).Body().Raw() - require.Equal(t, "{\"error\":{\"code\":\"PERMISSION_DENIED\",\"message\":\"You are not allowed to perform operation: /tigrisdata.management.v1.Management/CreateNamespace\"}}", resp) + require.JSONEq(t, `{"error":{"code":"PERMISSION_DENIED", + "message":"You are not allowed to perform operation: /tigrisdata.management.v1.Management/CreateNamespace"}}`, + resp) } func TestAuthzReadonly(t *testing.T) { @@ -884,11 +931,13 @@ func TestAuthzReadonly(t *testing.T) { resp := createProject2(t, "TestAuthzReadonly", token). Status(http.StatusForbidden).Body().Raw() - require.Equal(t, "{\"error\":{\"code\":\"PERMISSION_DENIED\",\"message\":\"You are not allowed to perform operation: /tigrisdata.v1.Tigris/CreateProject\"}}", resp) + require.JSONEq(t, `{"error":{"code":"PERMISSION_DENIED", + "message":"You are not allowed to perform operation: /tigrisdata.v1.Tigris/CreateProject"}}`, + resp) } func createProject2(t *testing.T, projectName string, token string) *httpexpect.Response { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) if token != "" { return e2.POST(getProjectURL(projectName, "create")). @@ -900,7 +949,7 @@ func createProject2(t *testing.T, projectName string, token string) *httpexpect. } func deleteProject2(t *testing.T, projectName string, token string) { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) _ = e2.DELETE(getProjectURL(projectName, "delete")). WithHeader(Authorization, Bearer+token). Expect() @@ -921,8 +970,9 @@ func getInvitationCode(t *testing.T, namespace string, email string) string { return "" } -func createUserInvitation(t *testing.T, email string, role string, invitationCreatedByName string, token string) *httpexpect.Response { - e2 := expectLow(t, config.GetBaseURL2()) +func createUserInvitation(t *testing.T, email string, role string, invitationCreatedByName string, token string, +) *httpexpect.Response { + e2 := expectAuthLow(t) invitationInfos := make([]api.InvitationInfo, 1) payload := make(map[string][]api.InvitationInfo) @@ -939,30 +989,22 @@ func createUserInvitation(t *testing.T, email string, role string, invitationCre } func listUserInvitations(t *testing.T, token string) *httpexpect.Response { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) return e2.GET(invitationUrl("list")). WithHeader(Authorization, Bearer+token). Expect() } -func listUsers(t *testing.T, token string) *httpexpect.Response { - e2 := expectLow(t, config.GetBaseURL2()) - - return e2.GET(listUsersUrl()). - WithHeader(Authorization, Bearer+token). - Expect() -} - func listProjects(t *testing.T, token string) *httpexpect.Response { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) return e2.GET(listProjectsUrl()). WithHeader(Authorization, Bearer+token). Expect() } func verifyUserInvitations(t *testing.T, email string, code string, token string, dry bool) *httpexpect.Response { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) payload := make(map[string]any) payload["email"] = email payload["code"] = code @@ -977,7 +1019,7 @@ func deleteUserInvitations(t *testing.T, email string, status string, token stri payload := make(map[string]string) payload["email"] = email payload["status"] = status - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) return e2.DELETE(invitationUrl("delete")). WithJSON(payload). WithHeader(Authorization, Bearer+token). @@ -988,10 +1030,6 @@ func invitationUrl(operation string) string { return fmt.Sprintf("/v1/auth/namespace/invitations/%s", operation) } -func listUsersUrl() string { - return "/v1/auth/namespace/users" -} - func listProjectsUrl() string { return "/v1/projects" } diff --git a/test/v1/server/management_test.go b/test/v1/server/management_test.go index e1c8c7d19..75c60a928 100644 --- a/test/v1/server/management_test.go +++ b/test/v1/server/management_test.go @@ -25,7 +25,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tigrisdata/tigris/test/config" "gopkg.in/gavv/httpexpect.v1" ) @@ -167,7 +166,7 @@ func namespaceInfoUrl(namespaceId string) string { return fmt.Sprintf("/v1/management/namespaces/%s", namespaceId) } func userMetaRequest(t *testing.T, token string, op string, key string, m Map) *httpexpect.Response { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) return e2.POST(getUserMetaURL(op, key)). WithHeader(Authorization, Bearer+token). WithJSON(m). @@ -175,7 +174,7 @@ func userMetaRequest(t *testing.T, token string, op string, key string, m Map) * } func nsMetaRequest(t *testing.T, token string, op string, key string, m Map) *httpexpect.Response { - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) return e2.POST(getNSMetaURL(op, key)). WithHeader(Authorization, Bearer+token). WithJSON(m). @@ -193,7 +192,7 @@ func getNSMetaURL(op string, key string) string { func TestUserMetadata(t *testing.T) { token := readToken(t, RSATokenFilePath) - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) _ = e2.POST(getCreateNamespaceURL()). WithHeader(Authorization, Bearer+token). WithJSON(Map{"name": "tigris_test", "id": "tigris_test"}). @@ -227,7 +226,7 @@ func TestUserMetadata(t *testing.T) { func TestNamespaceMetadata(t *testing.T) { token := readToken(t, RSATokenFilePath) - e2 := expectLow(t, config.GetBaseURL2()) + e2 := expectAuthLow(t) _ = e2.POST(getCreateNamespaceURL()). WithHeader(Authorization, Bearer+token). WithJSON(Map{"name": "tigris_test", "id": "tigris_test"}). diff --git a/test/v1/server/unix_test.go b/test/v1/server/unix_test.go new file mode 100644 index 000000000..6235d49ce --- /dev/null +++ b/test/v1/server/unix_test.go @@ -0,0 +1,92 @@ +// Copyright 2022-2023 Tigris Data, 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. + +//go:build (linux || darwin) && integration + +package server + +import ( + "context" + "fmt" + "net" + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/require" + config2 "github.com/tigrisdata/tigris/test/config" + "github.com/tigrisdata/tigris/util" + "gopkg.in/gavv/httpexpect.v1" +) + +func getUnixHTTPDialer(url string) func(_ context.Context, _, _ string) (net.Conn, error) { + var dialer func(_ context.Context, _, _ string) (net.Conn, error) + + if url[0] == '/' || url[0] == '.' { + dialer = func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", url) + } + } + + return dialer +} + +func TestUnixSocket(t *testing.T) { + t.Skip("socket only available when started using `make local_run`") + + db := fmt.Sprintf("integration_%s", t.Name()) + deleteProject(t, db) + createProject(t, db).Status(http.StatusOK) + + sock := "/tmp/tigris_test_server.sock" + + client := &http.Client{ + Transport: &http.Transport{ + DialContext: getUnixHTTPDialer(sock), + }, + } + + e := httpexpect.WithConfig(httpexpect.Config{ + Reporter: httpexpect.NewRequireReporter(t), + Client: client, + BaseURL: config2.GetBaseURL(), + }) + + e.DELETE(getCollectionURL(db, testCollection, "drop")). + Expect() + + e.POST(getCollectionURL(db, testCollection, "createOrUpdate")). + WithJSON(testCreateSchema). + Expect().Status(http.StatusOK) +} + +func TestReadPeerCreds(t *testing.T) { + sock := "/tmp/tigris_creds_test.sock" + _ = os.Remove(sock) + + l, err := net.Listen("unix", sock) + require.NoError(t, err) + + _, err = net.Dial("unix", sock) + require.NoError(t, err) + + conn, err := l.Accept() + require.NoError(t, err) + + creds, err := util.ReadPeerCreds(conn) + require.NoError(t, err) + + require.Equal(t, os.Geteuid(), int(creds.Uid)) + require.Equal(t, os.Getegid(), int(creds.Gid)) +} diff --git a/util/util.go b/util/util.go index f43e32b89..0a2c40e91 100644 --- a/util/util.go +++ b/util/util.go @@ -19,6 +19,7 @@ import ( "encoding/json" "fmt" "io" + "net" "os" "strings" "text/template" @@ -28,12 +29,15 @@ import ( "github.com/rs/zerolog/log" "github.com/tigrisdata/tigris/lib/container" ulog "github.com/tigrisdata/tigris/util/log" + "golang.org/x/sys/unix" ) const ( ObjFlattenDelimiter = "." ) +var ErrNotUnixConn = fmt.Errorf("expected unix socket connection") + // Version of this build. var Version string @@ -185,3 +189,31 @@ func RawMessageToByte(arr []jsoniter.RawMessage) [][]byte { ptr := unsafe.Pointer(&arr) return *(*[][]byte)(ptr) } + +func ReadPeerCreds(c net.Conn) (*unix.Ucred, error) { + var cred *unix.Ucred + + uc, ok := c.(*net.UnixConn) + if !ok { + return nil, ErrNotUnixConn + } + + raw, err := uc.SyscallConn() + if err != nil { + return nil, fmt.Errorf("error getting raw connection: %s", err) + } + + err1 := raw.Control(func(fd uintptr) { + cred, err = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) + }) + + if err != nil { + return nil, fmt.Errorf("getsockoptUcred error: %s", err) + } + + if err1 != nil { + return nil, fmt.Errorf("control error: %s", err1) + } + + return cred, nil +}