diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 028519bf4..35536d25e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,8 +121,7 @@ jobs: timeout-minutes: 20 env: UV_RESOLUTION: ${{ matrix.resolution }} - USES_SUPABASE_CLI: ${{ contains(fromJSON('["realtime", "storage"]'), matrix.package) }} - USES_INFRA: ${{ contains(fromJSON('["realtime", "storage", "auth", "postgrest"]'), matrix.package) }} + USES_SUPABASE_CLI: ${{ contains(fromJSON('["realtime", "storage", "auth", "postgrest"]'), matrix.package) }} steps: - name: Clone Repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -150,17 +149,17 @@ jobs: # fails the container start with "address already in use". - name: Reserve infra ports if: ${{ env.USES_SUPABASE_CLI == 'true' }} - run: sudo sysctl -w net.ipv4.ip_local_reserved_ports=54320-54329,55320-55329 + run: sudo sysctl -w net.ipv4.ip_local_reserved_ports=54320-54329,55320-55329,56320-56329,57320-57329 # Pulling images from the registries on every job gets throttled # (429 / "Data limit exceeded"), so reuse the images between runs. - name: Cache docker images - if: ${{ env.USES_INFRA == 'true' }} + if: ${{ env.USES_SUPABASE_CLI == 'true' }} id: docker-images uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: /tmp/docker-images - key: docker-images-${{ matrix.package }}-${{ steps.supabase-cli.outputs.version || 'compose' }}-${{ hashFiles(format('src/{0}/infra/**', matrix.package)) }} + key: docker-images-${{ matrix.package }}-${{ steps.supabase-cli.outputs.version }}-${{ hashFiles(format('src/{0}/infra/**', matrix.package)) }} - name: Load cached docker images if: ${{ steps.docker-images.outputs.cache-hit == 'true' }} @@ -177,7 +176,7 @@ jobs: # On a cache miss cache-hit is an empty string, not 'false'. - name: Save docker images to cache - if: ${{ env.USES_INFRA == 'true' && steps.docker-images.outputs.cache-hit != 'true' }} + if: ${{ env.USES_SUPABASE_CLI == 'true' && steps.docker-images.outputs.cache-hit != 'true' }} run: | mkdir -p /tmp/docker-images docker ps --all --format '{{.Image}}' | sort -u \ diff --git a/README.md b/README.md index 0b6545a5f..53bcfa183 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,7 @@ cd supabase-py This repository relies on the following dependencies for development: - `uv` for python project management. - `make` for running project commands. -- `docker` for both `postgrest` and `auth` test containers. -- `supabase-cli` for both `storage` and `realtime` test containers. +- `supabase-cli` and `docker` for the `auth`, `postgrest`, `storage` and `realtime` test containers. All of these dependencies are included in the nix shell environment, through `flake.nix`. If you've got `nix` installed, you may prefer to use it through `nix develop`. diff --git a/infra.mk b/infra.mk index 895cfae1d..5975be70d 100644 --- a/infra.mk +++ b/infra.mk @@ -25,5 +25,3 @@ endef # $(call supabase_cleanup,) supabase_cleanup = supabase --workdir infra stop --no-backup >/dev/null 2>&1 || true; \ docker ps -aq --filter "label=com.supabase.cli.project=$(1)" | xargs docker rm -f >/dev/null 2>&1 || true - -compose_cleanup = (cd infra && docker compose down --remove-orphans >/dev/null 2>&1) || true diff --git a/src/auth/Makefile b/src/auth/Makefile index 183728cd8..f36e66bf0 100644 --- a/src/auth/Makefile +++ b/src/auth/Makefile @@ -16,29 +16,26 @@ help:: include ../../infra.mk -start-infra: - @$(call retry_start_infra,$(compose_cleanup),(cd infra && docker compose up -d)) -help:: - @echo " start-infra -- start containers for tests" +AUTH_EXCLUDED_SERVICES := studio,postgrest,realtime,storage-api,imgproxy,edge-runtime,logflare,vector,supavisor,postgres-meta + +.DELETE_ON_ERROR: + +SIGNING_KEYS := infra/supabase/signing_keys.json -clean-infra: - cd infra &&\ - docker compose down --remove-orphans &&\ - docker system prune -a --volumes -f +$(SIGNING_KEYS): + echo '[]' > $@ + supabase --workdir infra gen signing-key --algorithm ES256 --append + +start-infra: $(SIGNING_KEYS) + @$(call retry_start_infra,$(call supabase_cleanup,auth-py-tests),supabase --workdir infra start -x $(AUTH_EXCLUDED_SERVICES)) help:: - @echo " clean-infra -- delete all stored information about the containers" + @echo " start-infra -- start containers for tests" stop-infra: - cd infra &&\ - docker compose down --remove-orphans + supabase --workdir infra stop help:: @echo " stop-infra -- stop containers for tests" -sync-infra: - uv run --package supabase_auth scripts/gh-download.py --repo=supabase/gotrue-js --branch=master --folder=infra -help:: - @echo " sync-infra -- update locked versions for test containers" - build-sync: uv run --package supabase_auth scripts/run-unasync.py help:: diff --git a/src/auth/infra/db/00-schema.sql b/src/auth/infra/db/00-schema.sql deleted file mode 100644 index 229af46f5..000000000 --- a/src/auth/infra/db/00-schema.sql +++ /dev/null @@ -1,85 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION postgres; --- auth.users definition -CREATE TABLE auth.users ( - instance_id uuid NULL, - id uuid NOT NULL, - aud varchar(255) NULL, - "role" varchar(255) NULL, - email varchar(255) NULL, - encrypted_password varchar(255) NULL, - confirmed_at timestamptz NULL, - invited_at timestamptz NULL, - confirmation_token varchar(255) NULL, - confirmation_sent_at timestamptz NULL, - recovery_token varchar(255) NULL, - recovery_sent_at timestamptz NULL, - email_change_token varchar(255) NULL, - email_change varchar(255) NULL, - email_change_sent_at timestamptz NULL, - last_sign_in_at timestamptz NULL, - raw_app_meta_data jsonb NULL, - raw_user_meta_data jsonb NULL, - is_super_admin bool NULL, - created_at timestamptz NULL, - updated_at timestamptz NULL, - CONSTRAINT users_pkey PRIMARY KEY (id) -); -CREATE INDEX users_instance_id_email_idx ON auth.users USING btree (instance_id, email); -CREATE INDEX users_instance_id_idx ON auth.users USING btree (instance_id); --- auth.refresh_tokens definition -CREATE TABLE auth.refresh_tokens ( - instance_id uuid NULL, - id bigserial NOT NULL, - "token" varchar(255) NULL, - user_id varchar(255) NULL, - revoked bool NULL, - created_at timestamptz NULL, - updated_at timestamptz NULL, - CONSTRAINT refresh_tokens_pkey PRIMARY KEY (id) -); -CREATE INDEX refresh_tokens_instance_id_idx ON auth.refresh_tokens USING btree (instance_id); -CREATE INDEX refresh_tokens_instance_id_user_id_idx ON auth.refresh_tokens USING btree (instance_id, user_id); -CREATE INDEX refresh_tokens_token_idx ON auth.refresh_tokens USING btree (token); --- auth.instances definition -CREATE TABLE auth.instances ( - id uuid NOT NULL, - uuid uuid NULL, - raw_base_config text NULL, - created_at timestamptz NULL, - updated_at timestamptz NULL, - CONSTRAINT instances_pkey PRIMARY KEY (id) -); --- auth.audit_log_entries definition -CREATE TABLE auth.audit_log_entries ( - instance_id uuid NULL, - id uuid NOT NULL, - payload json NULL, - created_at timestamptz NULL, - CONSTRAINT audit_log_entries_pkey PRIMARY KEY (id) -); -CREATE INDEX audit_logs_instance_id_idx ON auth.audit_log_entries USING btree (instance_id); --- auth.schema_migrations definition -CREATE TABLE auth.schema_migrations ( - "version" varchar(255) NOT NULL, - CONSTRAINT schema_migrations_pkey PRIMARY KEY ("version") -); -INSERT INTO auth.schema_migrations (version) -VALUES ('20171026211738'), - ('20171026211808'), - ('20171026211834'), - ('20180103212743'), - ('20180108183307'), - ('20180119214651'), - ('20180125194653'); --- Gets the User ID from the request cookie -create or replace function auth.uid() returns uuid as $$ - select nullif(current_setting('request.jwt.claim.sub', true), '')::uuid; -$$ language sql stable; --- Gets the User ID from the request cookie -create or replace function auth.role() returns text as $$ - select nullif(current_setting('request.jwt.claim.role', true), '')::text; -$$ language sql stable; -GRANT ALL PRIVILEGES ON SCHEMA auth TO postgres; -GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA auth TO postgres; -GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA auth TO postgres; -ALTER USER postgres SET search_path = "auth"; diff --git a/src/auth/infra/docker-compose.yml b/src/auth/infra/docker-compose.yml deleted file mode 100644 index 6d3096dbc..000000000 --- a/src/auth/infra/docker-compose.yml +++ /dev/null @@ -1,156 +0,0 @@ -# docker-compose.yml -name: auth-tests -services: - gotrue: # Signup enabled, autoconfirm off - image: supabase/auth:v2.187.0 - ports: - - '9999:9999' - environment: - GOTRUE_MAILER_URLPATHS_CONFIRMATION: '/verify' - GOTRUE_JWT_SECRET: '37c304f8-51aa-419a-a1af-06154e63707a' - GOTRUE_JWT_EXP: 3600 - GOTRUE_DB_DRIVER: postgres - DB_NAMESPACE: auth - GOTRUE_API_HOST: 0.0.0.0 - PORT: 9999 - GOTRUE_DISABLE_SIGNUP: 'false' - API_EXTERNAL_URL: http://localhost:9999 - GOTRUE_SITE_URL: http://localhost:9999 - GOTRUE_URI_ALLOW_LIST: https://supabase.io/docs - GOTRUE_MAILER_AUTOCONFIRM: 'false' - GOTRUE_LOG_LEVEL: DEBUG - GOTRUE_OPERATOR_TOKEN: super-secret-operator-token - DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable' - GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true' - GOTRUE_EXTERNAL_GOOGLE_ENABLED: 'true' - GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: 53566906701-bmhc1ndue7hild39575gkpimhs06b7ds.apps.googleusercontent.com - GOTRUE_EXTERNAL_GOOGLE_SECRET: Sm3s8RE85rDcS36iMy8YjrpC - GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: http://localhost:9999/callback - GOTRUE_SMTP_HOST: mail - GOTRUE_SMTP_PORT: 2500 - GOTRUE_SMTP_USER: GOTRUE_SMTP_USER - GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS - GOTRUE_SMTP_ADMIN_EMAIL: admin@email.com - GOTRUE_MAILER_SUBJECTS_CONFIRMATION: 'Please confirm' - GOTRUE_EXTERNAL_PHONE_ENABLED: 'true' - GOTRUE_SMS_PROVIDER: 'twilio' - GOTRUE_SMS_TWILIO_ACCOUNT_SID: '${GOTRUE_SMS_TWILIO_ACCOUNT_SID}' - GOTRUE_SMS_TWILIO_AUTH_TOKEN: '${GOTRUE_SMS_TWILIO_AUTH_TOKEN}' - GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID: '${GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID}' - GOTRUE_SMS_AUTOCONFIRM: 'false' - GOTRUE_COOKIE_KEY: 'sb' - depends_on: - - db - restart: on-failure - autoconfirm: # Signup enabled, autoconfirm on - image: supabase/auth:v2.187.0 - ports: - - '9998:9998' - environment: - GOTRUE_JWT_SECRET: '37c304f8-51aa-419a-a1af-06154e63707a' - GOTRUE_JWT_KEYS: '[{"kty":"oct","k":"Z7-AyPyChGNcQsX16cPBV-pPBo4q-zckDxkq1VZjATo","kid":"12580317-221c-49b6-894a-f4473b8afe39","key_ops":["sign", "verify"],"alg":"HS256"},{"kty":"RSA","n":"y3KQnIXK6wkPQ5m0XWp7z54BNZzXJk4IxXy81zFophdBBqz6u5OCMqWkC6i3WB7rlax4xjmxxyGyYRODooqCQTGahmpXryAAKc3g-gDIAq2MqVwlpmvXDavCVRK4hK7DZ6wK4MHrliSNHCuCkwIH3ofxTxgUwpSkOT58iU1ZOua5E1Y6R_Ozt3gLHha0Xa7a4V23pkP7n0xBvJPzIqiS3MZ4CQ_pz-buXYRgCPQkUJvXFFcuxmyqoYzorwQ1YVBOmH2XMx26RrCIxgj7geo9eVQ9u5qCPpQCGV5biqYMC4_m1kurOGf62URGRzXtmVzrW1PZJAeGoqMz5Fcfr8hiwQ","e":"AQAB","d":"C4XxquvpEmbw9mM-VAwz9w58Aw1fIkxJMuZdy9KAmue2RyqFCRrRxQycvgxQVi1qKpAaRx_9ccn20IjKa-psdkTY-8QKM2EcoUGH_KEOsxghX3ZYq5RwGdYgq7DjwqAjcTvNYe2Z6mcnlvDf9HOo_nG0uUYj5uGEa7meVCiNZUiSVdNGs-vOTUD8yB5pbZ4ute8ebuUzCWGQ3YwSNoWLa-dbECSO7jeobCapdB52MjEwE3_Ii8BWoySeDP-DEFX_5RTM2Zeh81zXAgmOxpZYTkjMsrznyxxBbXn7CdT8WMEXrreGZwIt3Mu6XpsLF5mwmTQ_ZyoM6tJpn5LeAhnCAQ","p":"6xy1skrnlrGUWtZFSHixn_eRA_O3GXKNBE4wziWodGZaFYsmFijZHbuQT0WFqc0epvLHNdNPvubFrVfV-U7ZIarfSSq6qBwBzDrDQS060MvjJIjrI16pKlx2X727FR1ZuwxT27dNg-wRTgKcZqXEalkvFOTEYBlCtw2-vzI0aRs","q":"3YWwOAs4GRZ9eq_fqNujACWJFyUO9QgEDPDOMg0EZhY7WkAlehTxxVXg65spWnfx_0GSc72I5N5qdbY-yDh2Dl7zIxvwnqZaKMJn4PEFkeAfyg62XlJlkHIwOVSj6vLNUDdDmG7bO2k6MyQ59jeuAemIljf9WhALNy8c9R0K3VM","dp":"KJ4LHcQnAjeng5Hk4kJHnXUtjls6VKEfj5DaiaKj2YgdI_-oEsf3ylUu9yLxloYjN4BVvgzFiBtiJzI3exyOEmzsqj1Bhe1guiGkvcvMj2nJ0fP9e1zNKM5UfPHQMjOh3tigXCLst0-_JZT55BnbNuw1YAytiFSU2_755xoLR-U","dq":"dCP7V-bJ6p1X_FLpOGau9wy262OKi_0_4mj-Mk-Q1tUhGRg4jeEdQRDdc6lN7Rilz-ZZGkVs2FGkD0MVd3PisXYmk2m6pfMhoe0K-WxkNy8Ce7Vq99jLVwgHMIenyS6zZjMTRYAZgPSShu2fVe-rU2VVLyz7r5RpzOzuibRIVfE","qi":"i7ND2teiVLkbaAs6rHfo5DiD1nlsORNYnn8Y_FjF6utb5OUljZ6-5WyEDJN9oIUX8o_Il9E6js-z7nhvPfFZHQN7ZWuYI0rO5qmsCDS9jWJ4GR61SgzZuLT7Jpp_KtwjW70x5wZ1Y-GugOP1Wct1YZWHn5YyLhvO6X_vttSmcS0","kid":"638c54b8-28c2-4b12-9598-ba12ef610a29","key_ops":["verify"],"alg":"RS256"}]' - GOTRUE_JWT_EXP: 3600 - GOTRUE_DB_DRIVER: postgres - DB_NAMESPACE: auth - GOTRUE_API_HOST: 0.0.0.0 - PORT: 9998 - GOTRUE_DISABLE_SIGNUP: 'false' - API_EXTERNAL_URL: http://localhost:9998 - GOTRUE_SITE_URL: http://localhost:9998 - GOTRUE_MAILER_AUTOCONFIRM: 'true' - GOTRUE_SMS_AUTOCONFIRM: 'true' - GOTRUE_LOG_LEVEL: DEBUG - GOTRUE_OPERATOR_TOKEN: super-secret-operator-token - DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable' - GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true' - GOTRUE_EXTERNAL_PHONE_ENABLED: 'true' - GOTRUE_SMTP_HOST: mail - GOTRUE_SMTP_PORT: 2500 - GOTRUE_SMTP_USER: GOTRUE_SMTP_USER - GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS - GOTRUE_SMTP_ADMIN_EMAIL: admin@email.com - GOTRUE_COOKIE_KEY: 'sb' - GOTRUE_OAUTH_SERVER_ENABLED: 'true' - GOTRUE_OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: 'true' - depends_on: - - db - restart: on-failure - autoconfirm_with_asymmetric_keys: # Signup enabled, autoconfirm on - image: supabase/auth:v2.187.0 - ports: - - '9996:9996' - environment: - GOTRUE_JWT_SECRET: 'Z7-AyPyChGNcQsX16cPBV-pPBo4q-zckDxkq1VZjATo' - GOTRUE_JWT_KEYS: '[{"kty":"oct","k":"Z7-AyPyChGNcQsX16cPBV-pPBo4q-zckDxkq1VZjATo","kid":"12580317-221c-49b6-894a-f4473b8afe39","key_ops":["verify"],"alg":"HS256"},{"kty":"RSA","n":"y3KQnIXK6wkPQ5m0XWp7z54BNZzXJk4IxXy81zFophdBBqz6u5OCMqWkC6i3WB7rlax4xjmxxyGyYRODooqCQTGahmpXryAAKc3g-gDIAq2MqVwlpmvXDavCVRK4hK7DZ6wK4MHrliSNHCuCkwIH3ofxTxgUwpSkOT58iU1ZOua5E1Y6R_Ozt3gLHha0Xa7a4V23pkP7n0xBvJPzIqiS3MZ4CQ_pz-buXYRgCPQkUJvXFFcuxmyqoYzorwQ1YVBOmH2XMx26RrCIxgj7geo9eVQ9u5qCPpQCGV5biqYMC4_m1kurOGf62URGRzXtmVzrW1PZJAeGoqMz5Fcfr8hiwQ","e":"AQAB","d":"C4XxquvpEmbw9mM-VAwz9w58Aw1fIkxJMuZdy9KAmue2RyqFCRrRxQycvgxQVi1qKpAaRx_9ccn20IjKa-psdkTY-8QKM2EcoUGH_KEOsxghX3ZYq5RwGdYgq7DjwqAjcTvNYe2Z6mcnlvDf9HOo_nG0uUYj5uGEa7meVCiNZUiSVdNGs-vOTUD8yB5pbZ4ute8ebuUzCWGQ3YwSNoWLa-dbECSO7jeobCapdB52MjEwE3_Ii8BWoySeDP-DEFX_5RTM2Zeh81zXAgmOxpZYTkjMsrznyxxBbXn7CdT8WMEXrreGZwIt3Mu6XpsLF5mwmTQ_ZyoM6tJpn5LeAhnCAQ","p":"6xy1skrnlrGUWtZFSHixn_eRA_O3GXKNBE4wziWodGZaFYsmFijZHbuQT0WFqc0epvLHNdNPvubFrVfV-U7ZIarfSSq6qBwBzDrDQS060MvjJIjrI16pKlx2X727FR1ZuwxT27dNg-wRTgKcZqXEalkvFOTEYBlCtw2-vzI0aRs","q":"3YWwOAs4GRZ9eq_fqNujACWJFyUO9QgEDPDOMg0EZhY7WkAlehTxxVXg65spWnfx_0GSc72I5N5qdbY-yDh2Dl7zIxvwnqZaKMJn4PEFkeAfyg62XlJlkHIwOVSj6vLNUDdDmG7bO2k6MyQ59jeuAemIljf9WhALNy8c9R0K3VM","dp":"KJ4LHcQnAjeng5Hk4kJHnXUtjls6VKEfj5DaiaKj2YgdI_-oEsf3ylUu9yLxloYjN4BVvgzFiBtiJzI3exyOEmzsqj1Bhe1guiGkvcvMj2nJ0fP9e1zNKM5UfPHQMjOh3tigXCLst0-_JZT55BnbNuw1YAytiFSU2_755xoLR-U","dq":"dCP7V-bJ6p1X_FLpOGau9wy262OKi_0_4mj-Mk-Q1tUhGRg4jeEdQRDdc6lN7Rilz-ZZGkVs2FGkD0MVd3PisXYmk2m6pfMhoe0K-WxkNy8Ce7Vq99jLVwgHMIenyS6zZjMTRYAZgPSShu2fVe-rU2VVLyz7r5RpzOzuibRIVfE","qi":"i7ND2teiVLkbaAs6rHfo5DiD1nlsORNYnn8Y_FjF6utb5OUljZ6-5WyEDJN9oIUX8o_Il9E6js-z7nhvPfFZHQN7ZWuYI0rO5qmsCDS9jWJ4GR61SgzZuLT7Jpp_KtwjW70x5wZ1Y-GugOP1Wct1YZWHn5YyLhvO6X_vttSmcS0","kid":"638c54b8-28c2-4b12-9598-ba12ef610a29","key_ops":["sign","verify"],"alg":"RS256"}]' - GOTRUE_JWT_EXP: 3600 - GOTRUE_DB_DRIVER: postgres - DB_NAMESPACE: auth - GOTRUE_API_HOST: 0.0.0.0 - PORT: 9996 - GOTRUE_DISABLE_SIGNUP: 'false' - API_EXTERNAL_URL: http://localhost:9996 - GOTRUE_SITE_URL: http://localhost:9996 - GOTRUE_MAILER_AUTOCONFIRM: 'true' - GOTRUE_SMS_AUTOCONFIRM: 'true' - GOTRUE_LOG_LEVEL: DEBUG - GOTRUE_OPERATOR_TOKEN: super-secret-operator-token - GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true' - DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable' - GOTRUE_EXTERNAL_PHONE_ENABLED: 'true' - GOTRUE_SMTP_HOST: mail - GOTRUE_SMTP_PORT: 2500 - GOTRUE_SMTP_USER: GOTRUE_SMTP_USER - GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS - GOTRUE_SMTP_ADMIN_EMAIL: admin@email.com - GOTRUE_COOKIE_KEY: 'sb' - depends_on: - - db - restart: on-failure - disabled: # Signup disabled - image: supabase/auth:v2.187.0 - ports: - - '9997:9997' - environment: - GOTRUE_JWT_SECRET: '37c304f8-51aa-419a-a1af-06154e63707a' - GOTRUE_JWT_EXP: 3600 - GOTRUE_DB_DRIVER: postgres - DB_NAMESPACE: auth - GOTRUE_API_HOST: 0.0.0.0 - PORT: 9997 - GOTRUE_DISABLE_SIGNUP: 'true' - API_EXTERNAL_URL: http://localhost:9997 - GOTRUE_SITE_URL: http://localhost:9997 - GOTRUE_MAILER_AUTOCONFIRM: 'false' - GOTRUE_LOG_LEVEL: DEBUG - GOTRUE_OPERATOR_TOKEN: super-secret-operator-token - DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable' - GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true' - GOTRUE_EXTERNAL_PHONE_ENABLED: 'false' - GOTRUE_EXTERNAL_EMAIL_ENABLED: 'false' - GOTRUE_SMTP_HOST: mail - GOTRUE_SMTP_PORT: 2500 - GOTRUE_SMTP_USER: GOTRUE_SMTP_USER - GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS - GOTRUE_SMTP_ADMIN_EMAIL: admin@email.com - GOTRUE_COOKIE_KEY: 'sb' - depends_on: - - db - restart: on-failure - mail: - image: phamhieu/inbucket:latest - ports: - - '2500:2500' # SMTP - - '9000:9000' # web interface - - '1100:1100' # POP3 - db: - image: supabase/postgres:15.1.1.46 - ports: - - '5432:5432' - command: postgres -c config_file=/etc/postgresql/postgresql.conf - volumes: - - ./db:/docker-entrypoint-initdb.d/ - environment: - POSTGRES_DB: postgres - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_PORT: 5432 diff --git a/src/auth/infra/supabase/.gitignore b/src/auth/infra/supabase/.gitignore new file mode 100644 index 000000000..382255f5b --- /dev/null +++ b/src/auth/infra/supabase/.gitignore @@ -0,0 +1,5 @@ +# Supabase +.branches +.temp +.env +signing_keys.json diff --git a/src/auth/infra/supabase/config.toml b/src/auth/infra/supabase/config.toml new file mode 100644 index 000000000..07148cfee --- /dev/null +++ b/src/auth/infra/supabase/config.toml @@ -0,0 +1,76 @@ +project_id = "auth-py-tests" + +[api] +enabled = true +port = 57321 +schemas = ["public"] +extra_search_path = ["public", "extensions"] +max_rows = 1000 + +[db] +port = 57322 +shadow_port = 57320 +major_version = 17 + +[db.seed] +enabled = false + +[local_smtp] +enabled = true +port = 57324 + +[auth] +enabled = true +site_url = "http://127.0.0.1:3000" +additional_redirect_urls = ["http://localhost:9999/welcome"] +jwt_expiry = 3600 +jwt_secret = "37c304f8-51aa-419a-a1af-06154e63707a" +signing_keys_path = "./signing_keys.json" +enable_refresh_token_rotation = true +refresh_token_reuse_interval = 10 +enable_signup = true +enable_anonymous_sign_ins = true +enable_manual_linking = false +minimum_password_length = 6 + +[auth.rate_limit] +email_sent = 10000 +sms_sent = 10000 +anonymous_users = 10000 +token_refresh = 10000 +sign_in_sign_ups = 10000 +token_verifications = 10000 +web3 = 10000 + +[auth.email] +enabled = true +double_confirm_changes = true +enable_confirmations = false +secure_password_change = false +max_frequency = "1s" + +[auth.sms] +enabled = true +enable_confirmations = false +template = "Your code is {{ .Code }}" +max_frequency = "1s" + +[auth.sms.twilio] +enabled = true +account_sid = "test" +message_service_sid = "test" +auth_token = "test" + +[auth.mfa.totp] +enroll_enabled = true +verify_enabled = true + +[auth.external.google] +enabled = true +client_id = "53566906701-bmhc1ndue7hild39575gkpimhs06b7ds.apps.googleusercontent.com" +secret = "Sm3s8RE85rDcS36iMy8YjrpC" + +[auth.oauth_server] +enabled = true +authorization_url_path = "/oauth/consent" +allow_dynamic_registration = true diff --git a/src/auth/pyproject.toml b/src/auth/pyproject.toml index a323a7795..897f684e6 100644 --- a/src/auth/pyproject.toml +++ b/src/auth/pyproject.toml @@ -31,9 +31,6 @@ repository = "https://github.com/supabase/supabase-py" documentation = "https://supabase.com/docs/reference/python/auth-api" changelog = "https://github.com/supabase/supabase-py/tree/main/CHANGELOG.md" -# [project.scripts] -# gh-download = "scripts.gh-download:main" - [dependency-groups] tests = [ "Faker >= 37.4.0", diff --git a/src/auth/scripts/gh-download.py b/src/auth/scripts/gh-download.py deleted file mode 100644 index b4ebbee79..000000000 --- a/src/auth/scripts/gh-download.py +++ /dev/null @@ -1,126 +0,0 @@ -# /// script -# dependencies = [ -# "pygithub >=1.57,<3.0", -# ] -# /// - -# This code was copied from -# https://gist.github.com/pdashford/2e4bcd4fc2343e2fd03efe4da17f577d -# and modified to work with Python 3, type hints, correct format and -# simplified the code to our needs. - -""" -Downloads folders from github repo -Requires PyGithub -pip install PyGithub -""" - -import base64 -import getopt -import os -import shutil -import sys - -from github import Github, GithubException -from github.ContentFile import ContentFile -from github.Repository import Repository - - -def get_sha_for_tag(repository: Repository, tag: str) -> str: - """ - Returns a commit PyGithub object for the specified repository and tag. - """ - branches = repository.get_branches() - matched_branches = [match for match in branches if match.name == tag] - if matched_branches: - return matched_branches[0].commit.sha - - tags = repository.get_tags() - matched_tags = [match for match in tags if match.name == tag] - if not matched_tags: - raise ValueError("No Tag or Branch exists with that name") - return matched_tags[0].commit.sha - - -def download_directory(repository: Repository, sha: str, server_path: str) -> None: - """ - Download all contents at server_path with commit tag sha in - the repository. - """ - if os.path.exists(server_path): - shutil.rmtree(server_path) - - os.makedirs(server_path) - contents = repository.get_dir_contents(server_path, ref=sha) - - for content in contents: - print(f"Processing {content.path}") - if content.type == "dir": - os.makedirs(content.path) - download_directory(repository, sha, content.path) - else: - try: - path = content.path - file_content = repository.get_contents(path, ref=sha) - if not isinstance(file_content, ContentFile): - raise ValueError("Expected ContentFile") - with open(content.path, "w+") as file_out: - if file_content.content: - file_data = base64.b64decode(file_content.content) - file_out.write(file_data.decode("utf-8")) - except (GithubException, OSError, ValueError) as exc: - print("Error processing %s: %s", content.path, exc) - - -def usage() -> None: - """ - Prints the usage command lines - """ - print("usage: gh-download --repo=repo --branch=branch --folder=folder") - - -def main(argv: list[str]) -> None: - """ - Main function block - """ - try: - opts, _ = getopt.getopt(argv, "r:b:f:", ["repo=", "branch=", "folder="]) - except getopt.GetoptError as err: - print(err) - usage() - sys.exit(2) - repo: str | None = None - branch: str | None = None - folder: str | None = None - for opt, arg in opts: - if opt in ("-r", "--repo"): - repo = arg - elif opt in ("-b", "--branch"): - branch = arg - elif opt in ("-f", "--folder"): - folder = arg - - if not repo: - print("Repo is required") - usage() - sys.exit(2) - if not branch: - print("Branch is required") - usage() - sys.exit(2) - if not folder: - print("Folder is required") - usage() - sys.exit(2) - - github = Github(None) - repository = github.get_repo(repo) - sha = get_sha_for_tag(repository, branch) - download_directory(repository, sha, folder) - - -if __name__ == "__main__": - """ - Entry point - """ - main(sys.argv[1:]) diff --git a/src/auth/tests/_async/conftest.py b/src/auth/tests/_async/conftest.py index dff80dd70..5982042a7 100644 --- a/src/auth/tests/_async/conftest.py +++ b/src/auth/tests/_async/conftest.py @@ -70,34 +70,10 @@ def mock_app_metadata() -> dict[str, list[str]]: } -SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT = 9999 -SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT = 9998 -SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT = 9997 -SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON_PORT = 9996 - -GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF = ( - f"http://localhost:{SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT}" -) -GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON = ( - f"http://localhost:{SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT}" -) -GOTRUE_URL_SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON = ( - f"http://localhost:{SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON_PORT}" -) -GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF = ( - f"http://localhost:{SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT}" -) +AUTH_URL = "http://127.0.0.1:57321/auth/v1" GOTRUE_JWT_SECRET = "37c304f8-51aa-419a-a1af-06154e63707a" -AUTH_ADMIN_JWT = encode( - { - "sub": "1234567890", - "role": "supabase_admin", - }, - GOTRUE_JWT_SECRET, -) - @pytest.fixture async def admin_client_with_user_and_credentials( @@ -116,7 +92,7 @@ async def auth_client( request: pytest.FixtureRequest, ) -> AsyncGenerator[AsyncSupabaseAuthClient]: async with AsyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + url=AUTH_URL, auto_refresh_token=False, http_session=request.param(), ) as client: @@ -128,7 +104,7 @@ async def auth_client_with_session( request: pytest.FixtureRequest, ) -> AsyncGenerator[AsyncSupabaseAuthClient]: async with AsyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + url=AUTH_URL, auto_refresh_token=False, http_session=request.param(), ) as client: @@ -147,94 +123,6 @@ async def auth_client_with_session( await client.sign_out() -@pytest.fixture(params=http_sessions) -async def auth_client_with_asymmetric_session( - request: pytest.FixtureRequest, -) -> AsyncGenerator[AsyncSupabaseAuthClient]: - async with AsyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON, - auto_refresh_token=False, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -async def auth_subscription_client( - request: pytest.FixtureRequest, -) -> AsyncGenerator[AsyncSupabaseAuthClient]: - async with AsyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - auto_refresh_token=False, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -async def client_api_auto_confirm_enabled_client( - request: pytest.FixtureRequest, -) -> AsyncGenerator[AsyncSupabaseAuthClient]: - async with AsyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - auto_refresh_token=False, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -async def client_api_auto_confirm_off_signups_enabled_client( - request: pytest.FixtureRequest, -) -> AsyncGenerator[AsyncSupabaseAuthClient]: - async with AsyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, - auto_refresh_token=False, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -async def client_api_auto_confirm_disabled_client( - request: pytest.FixtureRequest, -) -> AsyncGenerator[AsyncSupabaseAuthClient]: - async with AsyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, - auto_refresh_token=False, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -async def auth_admin_api_auto_confirm_enabled_client( - request: pytest.FixtureRequest, -) -> AsyncGenerator[AsyncSupabaseAuthAdmin]: - async with AsyncSupabaseAuthAdmin( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - default_headers={ - "Authorization": f"Bearer {AUTH_ADMIN_JWT}", - }, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -async def auth_admin_api_auto_confirm_disabled_client( - request: pytest.FixtureRequest, -) -> AsyncGenerator[AsyncSupabaseAuthAdmin]: - async with AsyncSupabaseAuthAdmin( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, - default_headers={ - "Authorization": f"Bearer {AUTH_ADMIN_JWT}", - }, - http_session=request.param(), - ) as client: - yield client - - SECRET_KEY_JWT = encode( { "role": "service_role", @@ -248,35 +136,7 @@ async def secret_key_api_client( request: pytest.FixtureRequest, ) -> AsyncGenerator[AsyncSupabaseAuthAdmin]: async with AsyncSupabaseAuthAdmin( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - default_headers={ - "Authorization": f"Bearer {SECRET_KEY_JWT}", - }, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -async def secret_key_api_client_with_sms( - request: pytest.FixtureRequest, -) -> AsyncGenerator[AsyncSupabaseAuthAdmin]: - async with AsyncSupabaseAuthAdmin( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, - default_headers={ - "Authorization": f"Bearer {SECRET_KEY_JWT}", - }, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -async def secret_key_api_client_no_sms( - request: pytest.FixtureRequest, -) -> AsyncGenerator[AsyncSupabaseAuthAdmin]: - async with AsyncSupabaseAuthAdmin( - url=GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, + url=AUTH_URL, default_headers={ "Authorization": f"Bearer {SECRET_KEY_JWT}", }, diff --git a/src/auth/tests/_async/test_gotrue.py b/src/auth/tests/_async/test_gotrue.py index 62f1a699c..92cd923ef 100644 --- a/src/auth/tests/_async/test_gotrue.py +++ b/src/auth/tests/_async/test_gotrue.py @@ -47,53 +47,56 @@ async def test_get_claims_returns_none_when_session_is_none( async def test_get_claims_calls_get_user_if_symmetric_jwt( mocker: MockerFixture, auth_client: AsyncSupabaseAuthClient ) -> None: - spy = mocker.spy(auth_client.session_manager, "_get_user") credentials = mock_user_credentials() options = SignUpWithPassword.email( email=credentials.email, password=credentials.password, ) - user = (await auth_client.sign_up(options)).user + response = await auth_client.sign_up(options) + assert response.user is not None + assert response.session is not None + payload = decode_jwt(response.session.access_token).payload + symmetric_jwt = encode( + payload.model_dump(exclude_none=True), GOTRUE_JWT_SECRET, algorithm="HS256" + ) + spy = mocker.spy(auth_client.session_manager, "_get_user") - assert user is not None - - response = await auth_client.get_claims() - assert response - claims = response.claims + claims_response = await auth_client.get_claims(symmetric_jwt) + assert claims_response + claims = claims_response.claims assert claims.model_extra - assert claims.model_extra["email"] == user.email + assert claims.model_extra["email"] == response.user.email spy.assert_called_once() async def test_get_claims_fetches_jwks_to_verify_asymmetric_jwt( - mocker: MockerFixture, auth_client_with_asymmetric_session: AsyncSupabaseAuthClient + mocker: MockerFixture, auth_client: AsyncSupabaseAuthClient ) -> None: credentials = mock_user_credentials() options = SignUpWithPassword.email( email=credentials.email, password=credentials.password, ) - user = (await auth_client_with_asymmetric_session.sign_up(options)).user + user = (await auth_client.sign_up(options)).user assert user is not None - response = await auth_client_with_asymmetric_session.get_claims() + response = await auth_client.get_claims() assert response claims = response.claims assert claims.model_extra assert claims.model_extra["email"] == user.email - expected_keyid = "638c54b8-28c2-4b12-9598-ba12ef610a29" - - assert len(auth_client_with_asymmetric_session._jwks.keys) == 1 - assert auth_client_with_asymmetric_session._jwks.keys[0].kid == expected_keyid + assert response.headers.alg == "ES256" + assert len(auth_client._jwks.keys) == 1 + assert auth_client._jwks.keys[0].kid == response.headers.kid async def test_jwks_ttl_cache_behavior( - mocker: MockerFixture, auth_client_with_asymmetric_session: AsyncSupabaseAuthClient + mocker: MockerFixture, auth_client: AsyncSupabaseAuthClient ) -> None: - spy = mocker.spy(auth_client_with_asymmetric_session.executor.session, "send") + spy = mocker.spy(auth_client.executor.session, "send") # First call should fetch JWKS from endpoint credentials = mock_user_credentials() @@ -101,15 +104,15 @@ async def test_jwks_ttl_cache_behavior( email=credentials.email, password=credentials.password, ) - user = (await auth_client_with_asymmetric_session.sign_up(options)).user + user = (await auth_client.sign_up(options)).user assert user is not None - await auth_client_with_asymmetric_session.get_claims() + await auth_client.get_claims() first_call_count = spy.call_count # Second call within TTL should use cache - await auth_client_with_asymmetric_session.get_claims() + await auth_client.get_claims() assert spy.call_count == first_call_count # No additional JWKS request # Mock time to be after TTL expiry @@ -119,7 +122,7 @@ async def test_jwks_ttl_cache_behavior( mock_time.return_value = original_time() + 601 # TTL is 600 seconds # Call after TTL expiry should fetch fresh JWKS - await auth_client_with_asymmetric_session.get_claims() + await auth_client.get_claims() assert spy.call_count == first_call_count + 1 # One more JWKS request finally: # Restore original time function diff --git a/src/auth/tests/_async/test_gotrue_admin_api.py b/src/auth/tests/_async/test_gotrue_admin_api.py index 99acbdd42..d18dea06a 100644 --- a/src/auth/tests/_async/test_gotrue_admin_api.py +++ b/src/auth/tests/_async/test_gotrue_admin_api.py @@ -134,16 +134,14 @@ async def test_modify_app_metadata_using_update_user_by_id( async def test_modify_confirm_email_using_update_user_by_id( secret_key_api_client: AsyncSupabaseAuthAdmin, - client_api_auto_confirm_off_signups_enabled_client: AsyncSupabaseAuthClient, ) -> None: credentials = mock_user_credentials() - response = await client_api_auto_confirm_off_signups_enabled_client.sign_up( - SignUpWithPassword.email( + response = await secret_key_api_client.create_user( + AdminUserAttributes( email=credentials.email, password=credentials.password, ) ) - assert response.user assert not response.user.email_confirmed_at auth_response = await secret_key_api_client.update_user_by_id( response.user.id, @@ -156,11 +154,9 @@ async def test_modify_confirm_email_using_update_user_by_id( async def test_resend( - client_api_auto_confirm_off_signups_enabled_client: AsyncSupabaseAuthClient, + auth_client: AsyncSupabaseAuthClient, ) -> None: - await client_api_auto_confirm_off_signups_enabled_client.resend( - Resend.phone(phone="+112345678", type="sms") - ) + await auth_client.resend(Resend.phone(phone="+112345678", type="sms")) async def test_reauthenticate( @@ -183,12 +179,10 @@ async def test_reset_password_for_email( async def test_resend_missing_credentials( - client_api_auto_confirm_off_signups_enabled_client: AsyncSupabaseAuthClient, + auth_client: AsyncSupabaseAuthClient, ) -> None: credentials = mock_user_credentials() - await client_api_auto_confirm_off_signups_enabled_client.resend( - Resend.email(type="email_change", email=credentials.email) - ) + await auth_client.resend(Resend.email(type="email_change", email=credentials.email)) async def test_sign_in_anonymously( @@ -270,11 +264,9 @@ async def test_invite_user_by_email_creates_a_new_user_with_an_invited_at_timest async def test_sign_in_with_oauth( - client_api_auto_confirm_off_signups_enabled_client: AsyncSupabaseAuthClient, + auth_client: AsyncSupabaseAuthClient, ) -> None: - assert await client_api_auto_confirm_off_signups_enabled_client.sign_in_with_oauth( - provider="google" - ) + assert await auth_client.sign_in_with_oauth(provider="google") async def test_get_item_from_memory_storage( diff --git a/src/auth/tests/_sync/conftest.py b/src/auth/tests/_sync/conftest.py index 36d935f37..63a62b6db 100644 --- a/src/auth/tests/_sync/conftest.py +++ b/src/auth/tests/_sync/conftest.py @@ -70,34 +70,10 @@ def mock_app_metadata() -> dict[str, list[str]]: } -SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT = 9999 -SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT = 9998 -SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT = 9997 -SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON_PORT = 9996 - -GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF = ( - f"http://localhost:{SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT}" -) -GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON = ( - f"http://localhost:{SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT}" -) -GOTRUE_URL_SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON = ( - f"http://localhost:{SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON_PORT}" -) -GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF = ( - f"http://localhost:{SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT}" -) +AUTH_URL = "http://127.0.0.1:57321/auth/v1" GOTRUE_JWT_SECRET = "37c304f8-51aa-419a-a1af-06154e63707a" -AUTH_ADMIN_JWT = encode( - { - "sub": "1234567890", - "role": "supabase_admin", - }, - GOTRUE_JWT_SECRET, -) - @pytest.fixture def admin_client_with_user_and_credentials( @@ -116,7 +92,7 @@ def auth_client( request: pytest.FixtureRequest, ) -> Generator[SyncSupabaseAuthClient]: with SyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + url=AUTH_URL, auto_refresh_token=False, http_session=request.param(), ) as client: @@ -128,7 +104,7 @@ def auth_client_with_session( request: pytest.FixtureRequest, ) -> Generator[SyncSupabaseAuthClient]: with SyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + url=AUTH_URL, auto_refresh_token=False, http_session=request.param(), ) as client: @@ -147,94 +123,6 @@ def auth_client_with_session( client.sign_out() -@pytest.fixture(params=http_sessions) -def auth_client_with_asymmetric_session( - request: pytest.FixtureRequest, -) -> Generator[SyncSupabaseAuthClient]: - with SyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON, - auto_refresh_token=False, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -def auth_subscription_client( - request: pytest.FixtureRequest, -) -> Generator[SyncSupabaseAuthClient]: - with SyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - auto_refresh_token=False, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -def client_api_auto_confirm_enabled_client( - request: pytest.FixtureRequest, -) -> Generator[SyncSupabaseAuthClient]: - with SyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - auto_refresh_token=False, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -def client_api_auto_confirm_off_signups_enabled_client( - request: pytest.FixtureRequest, -) -> Generator[SyncSupabaseAuthClient]: - with SyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, - auto_refresh_token=False, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -def client_api_auto_confirm_disabled_client( - request: pytest.FixtureRequest, -) -> Generator[SyncSupabaseAuthClient]: - with SyncSupabaseAuthClient( - url=GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, - auto_refresh_token=False, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -def auth_admin_api_auto_confirm_enabled_client( - request: pytest.FixtureRequest, -) -> Generator[SyncSupabaseAuthAdmin]: - with SyncSupabaseAuthAdmin( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - default_headers={ - "Authorization": f"Bearer {AUTH_ADMIN_JWT}", - }, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -def auth_admin_api_auto_confirm_disabled_client( - request: pytest.FixtureRequest, -) -> Generator[SyncSupabaseAuthAdmin]: - with SyncSupabaseAuthAdmin( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, - default_headers={ - "Authorization": f"Bearer {AUTH_ADMIN_JWT}", - }, - http_session=request.param(), - ) as client: - yield client - - SECRET_KEY_JWT = encode( { "role": "service_role", @@ -248,35 +136,7 @@ def secret_key_api_client( request: pytest.FixtureRequest, ) -> Generator[SyncSupabaseAuthAdmin]: with SyncSupabaseAuthAdmin( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - default_headers={ - "Authorization": f"Bearer {SECRET_KEY_JWT}", - }, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -def secret_key_api_client_with_sms( - request: pytest.FixtureRequest, -) -> Generator[SyncSupabaseAuthAdmin]: - with SyncSupabaseAuthAdmin( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, - default_headers={ - "Authorization": f"Bearer {SECRET_KEY_JWT}", - }, - http_session=request.param(), - ) as client: - yield client - - -@pytest.fixture(params=http_sessions) -def secret_key_api_client_no_sms( - request: pytest.FixtureRequest, -) -> Generator[SyncSupabaseAuthAdmin]: - with SyncSupabaseAuthAdmin( - url=GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, + url=AUTH_URL, default_headers={ "Authorization": f"Bearer {SECRET_KEY_JWT}", }, diff --git a/src/auth/tests/_sync/test_gotrue.py b/src/auth/tests/_sync/test_gotrue.py index c06e55a58..675bed25a 100644 --- a/src/auth/tests/_sync/test_gotrue.py +++ b/src/auth/tests/_sync/test_gotrue.py @@ -47,53 +47,56 @@ def test_get_claims_returns_none_when_session_is_none( def test_get_claims_calls_get_user_if_symmetric_jwt( mocker: MockerFixture, auth_client: SyncSupabaseAuthClient ) -> None: - spy = mocker.spy(auth_client.session_manager, "_get_user") credentials = mock_user_credentials() options = SignUpWithPassword.email( email=credentials.email, password=credentials.password, ) - user = (auth_client.sign_up(options)).user + response = auth_client.sign_up(options) + assert response.user is not None + assert response.session is not None + payload = decode_jwt(response.session.access_token).payload + symmetric_jwt = encode( + payload.model_dump(exclude_none=True), GOTRUE_JWT_SECRET, algorithm="HS256" + ) + spy = mocker.spy(auth_client.session_manager, "_get_user") - assert user is not None - - response = auth_client.get_claims() - assert response - claims = response.claims + claims_response = auth_client.get_claims(symmetric_jwt) + assert claims_response + claims = claims_response.claims assert claims.model_extra - assert claims.model_extra["email"] == user.email + assert claims.model_extra["email"] == response.user.email spy.assert_called_once() def test_get_claims_fetches_jwks_to_verify_asymmetric_jwt( - mocker: MockerFixture, auth_client_with_asymmetric_session: SyncSupabaseAuthClient + mocker: MockerFixture, auth_client: SyncSupabaseAuthClient ) -> None: credentials = mock_user_credentials() options = SignUpWithPassword.email( email=credentials.email, password=credentials.password, ) - user = (auth_client_with_asymmetric_session.sign_up(options)).user + user = (auth_client.sign_up(options)).user assert user is not None - response = auth_client_with_asymmetric_session.get_claims() + response = auth_client.get_claims() assert response claims = response.claims assert claims.model_extra assert claims.model_extra["email"] == user.email - expected_keyid = "638c54b8-28c2-4b12-9598-ba12ef610a29" - - assert len(auth_client_with_asymmetric_session._jwks.keys) == 1 - assert auth_client_with_asymmetric_session._jwks.keys[0].kid == expected_keyid + assert response.headers.alg == "ES256" + assert len(auth_client._jwks.keys) == 1 + assert auth_client._jwks.keys[0].kid == response.headers.kid def test_jwks_ttl_cache_behavior( - mocker: MockerFixture, auth_client_with_asymmetric_session: SyncSupabaseAuthClient + mocker: MockerFixture, auth_client: SyncSupabaseAuthClient ) -> None: - spy = mocker.spy(auth_client_with_asymmetric_session.executor.session, "send") + spy = mocker.spy(auth_client.executor.session, "send") # First call should fetch JWKS from endpoint credentials = mock_user_credentials() @@ -101,15 +104,15 @@ def test_jwks_ttl_cache_behavior( email=credentials.email, password=credentials.password, ) - user = (auth_client_with_asymmetric_session.sign_up(options)).user + user = (auth_client.sign_up(options)).user assert user is not None - auth_client_with_asymmetric_session.get_claims() + auth_client.get_claims() first_call_count = spy.call_count # Second call within TTL should use cache - auth_client_with_asymmetric_session.get_claims() + auth_client.get_claims() assert spy.call_count == first_call_count # No additional JWKS request # Mock time to be after TTL expiry @@ -119,7 +122,7 @@ def test_jwks_ttl_cache_behavior( mock_time.return_value = original_time() + 601 # TTL is 600 seconds # Call after TTL expiry should fetch fresh JWKS - auth_client_with_asymmetric_session.get_claims() + auth_client.get_claims() assert spy.call_count == first_call_count + 1 # One more JWKS request finally: # Restore original time function diff --git a/src/auth/tests/_sync/test_gotrue_admin_api.py b/src/auth/tests/_sync/test_gotrue_admin_api.py index 91f27d45b..1d523caa6 100644 --- a/src/auth/tests/_sync/test_gotrue_admin_api.py +++ b/src/auth/tests/_sync/test_gotrue_admin_api.py @@ -134,16 +134,14 @@ def test_modify_app_metadata_using_update_user_by_id( def test_modify_confirm_email_using_update_user_by_id( secret_key_api_client: SyncSupabaseAuthAdmin, - client_api_auto_confirm_off_signups_enabled_client: SyncSupabaseAuthClient, ) -> None: credentials = mock_user_credentials() - response = client_api_auto_confirm_off_signups_enabled_client.sign_up( - SignUpWithPassword.email( + response = secret_key_api_client.create_user( + AdminUserAttributes( email=credentials.email, password=credentials.password, ) ) - assert response.user assert not response.user.email_confirmed_at auth_response = secret_key_api_client.update_user_by_id( response.user.id, @@ -156,11 +154,9 @@ def test_modify_confirm_email_using_update_user_by_id( def test_resend( - client_api_auto_confirm_off_signups_enabled_client: SyncSupabaseAuthClient, + auth_client: SyncSupabaseAuthClient, ) -> None: - client_api_auto_confirm_off_signups_enabled_client.resend( - Resend.phone(phone="+112345678", type="sms") - ) + auth_client.resend(Resend.phone(phone="+112345678", type="sms")) def test_reauthenticate( @@ -183,12 +179,10 @@ def test_reset_password_for_email( def test_resend_missing_credentials( - client_api_auto_confirm_off_signups_enabled_client: SyncSupabaseAuthClient, + auth_client: SyncSupabaseAuthClient, ) -> None: credentials = mock_user_credentials() - client_api_auto_confirm_off_signups_enabled_client.resend( - Resend.email(type="email_change", email=credentials.email) - ) + auth_client.resend(Resend.email(type="email_change", email=credentials.email)) def test_sign_in_anonymously( @@ -270,11 +264,9 @@ def test_invite_user_by_email_creates_a_new_user_with_an_invited_at_timestamp( def test_sign_in_with_oauth( - client_api_auto_confirm_off_signups_enabled_client: SyncSupabaseAuthClient, + auth_client: SyncSupabaseAuthClient, ) -> None: - assert client_api_auto_confirm_off_signups_enabled_client.sign_in_with_oauth( - provider="google" - ) + assert auth_client.sign_in_with_oauth(provider="google") def test_get_item_from_memory_storage( diff --git a/src/postgrest/Makefile b/src/postgrest/Makefile index 927b6f305..44369c736 100644 --- a/src/postgrest/Makefile +++ b/src/postgrest/Makefile @@ -18,24 +18,18 @@ help:: include ../../infra.mk +POSTGREST_EXCLUDED_SERVICES := studio,gotrue,mailpit,realtime,storage-api,imgproxy,edge-runtime,logflare,vector,supavisor,postgres-meta + start-infra: - @$(call retry_start_infra,$(compose_cleanup),(cd infra && docker compose up -d)) + @$(call retry_start_infra,$(call supabase_cleanup,postgrest-py-tests),supabase --workdir infra start -x $(POSTGREST_EXCLUDED_SERVICES)) help:: - @echo " stop-infra -- start containers for tests" + @echo " start-infra -- start containers for tests" stop-infra: - cd infra &&\ - docker compose down --remove-orphans + supabase --workdir infra stop help:: @echo " stop-infra -- stop containers for tests" -clean-infra: - cd infra &&\ - docker compose down --remove-orphans &&\ - docker system prune -a --volumes -f -help:: - @echo " clean-infra -- delete all stored information about the containers" - clean: rm -rf htmlcov .pytest_cache .ruff_cache rm -f .coverage coverage.xml diff --git a/src/postgrest/README.md b/src/postgrest/README.md index 8a7a8f63f..b433a4ed8 100644 --- a/src/postgrest/README.md +++ b/src/postgrest/README.md @@ -12,13 +12,13 @@ ### Local PostgREST server -If you want to use a local PostgREST server for development, you can use our preconfigured instance via Docker Compose. +If you want to use a local PostgREST server for development, you can start our preconfigured instance with the [Supabase CLI](https://supabase.com/docs/guides/local-development/cli/getting-started). ```sh -docker compose -f infra/docker-compose.yaml up +supabase --workdir infra start ``` -Once Docker Compose started, PostgREST is accessible at . +Once it has started, PostgREST is accessible at . ### Instructions @@ -58,7 +58,7 @@ from supabase_utils.http.adapters.httpx import AsyncHttpxSession async def main() -> None: async with AsyncPostgrestClient( - "http://localhost:3000", AsyncHttpxSession(AsyncClient()) + "http://127.0.0.1:56321/rest/v1", AsyncHttpxSession(AsyncClient()) ) as client: r = await client.from_("countries").select("*").execute() countries = r.data diff --git a/src/postgrest/infra/docker-compose.yaml b/src/postgrest/infra/docker-compose.yaml deleted file mode 100644 index 30d847e14..000000000 --- a/src/postgrest/infra/docker-compose.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# docker-compose.yml -name: postgrest-tests -services: - rest: - image: postgrest/postgrest:v11.2.2 - ports: - - '3000:3000' - environment: - PGRST_DB_URI: postgres://postgres:postgres@db:5432/postgres - PGRST_DB_SCHEMAS: public,personal - PGRST_DB_EXTRA_SEARCH_PATH: extensions - PGRST_DB_ANON_ROLE: postgres - PGRST_DB_PLAN_ENABLED: 1 - PGRST_DB_TX_END: commit-allow-override - depends_on: - - db - db: - image: supabase/postgres:15.1.0.37 - ports: - - '5732:5732' - volumes: - - .:/docker-entrypoint-initdb.d/ - environment: - POSTGRES_DB: postgres - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_HOST: /var/run/postgresql - POSTGRES_PORT: 5732 diff --git a/src/postgrest/infra/supabase/.gitignore b/src/postgrest/infra/supabase/.gitignore new file mode 100644 index 000000000..a3ad88055 --- /dev/null +++ b/src/postgrest/infra/supabase/.gitignore @@ -0,0 +1,4 @@ +# Supabase +.branches +.temp +.env diff --git a/src/postgrest/infra/supabase/config.toml b/src/postgrest/infra/supabase/config.toml new file mode 100644 index 000000000..bcc33497e --- /dev/null +++ b/src/postgrest/infra/supabase/config.toml @@ -0,0 +1,16 @@ +project_id = "postgrest-py-tests" + +[api] +enabled = true +port = 56321 +schemas = ["public"] +extra_search_path = ["public", "extensions"] +max_rows = 1000 + +[db] +port = 56322 +shadow_port = 56320 +major_version = 17 + +[db.seed] +enabled = false diff --git a/src/postgrest/infra/init.sql b/src/postgrest/infra/supabase/migrations/20240101000000_init.sql similarity index 94% rename from src/postgrest/infra/init.sql rename to src/postgrest/infra/supabase/migrations/20240101000000_init.sql index 20edeceec..77bbbf009 100644 --- a/src/postgrest/infra/init.sql +++ b/src/postgrest/infra/supabase/migrations/20240101000000_init.sql @@ -103,3 +103,8 @@ insert into values (1, 1, 'harp'), (2, 1, 'violin'); + +grant select, insert, update, delete on all tables in schema public to anon; +grant execute on all functions in schema public to anon; + +alter role authenticator set pgrst.db_plan_enabled = 'true'; diff --git a/src/postgrest/tests/_async/conftest.py b/src/postgrest/tests/_async/conftest.py index af2905e8a..59600d98b 100644 --- a/src/postgrest/tests/_async/conftest.py +++ b/src/postgrest/tests/_async/conftest.py @@ -8,7 +8,7 @@ from postgrest import AsyncPostgrestClient -REST_URL = "http://127.0.0.1:3000" +REST_URL = "http://127.0.0.1:56321/rest/v1" def httpx_client() -> AsyncClient: diff --git a/src/postgrest/tests/_sync/conftest.py b/src/postgrest/tests/_sync/conftest.py index 96083d5da..cf5f9da14 100644 --- a/src/postgrest/tests/_sync/conftest.py +++ b/src/postgrest/tests/_sync/conftest.py @@ -6,7 +6,7 @@ from postgrest import SyncPostgrestClient -REST_URL = "http://127.0.0.1:3000" +REST_URL = "http://127.0.0.1:56321/rest/v1" def httpx_client() -> Client: