From 8f2103b7de5c2f0e595c8f5210518853aca5d237 Mon Sep 17 00:00:00 2001 From: Sarah Funkhouser <147884153+golanglemonade@users.noreply.github.com> Date: Sun, 31 May 2026 13:08:52 -0600 Subject: [PATCH] update migration steps, contact test, default sso config var Signed-off-by: Sarah Funkhouser <147884153+golanglemonade@users.noreply.github.com> --- .github/workflows/releaser.yml | 4 +- config/.env.example | 2 +- config/config.example.yaml | 2 +- config/helm-values.yaml | 2 +- db/MIGRATION.md | 80 ++++++++++++++++------ docker/docker-compose-fga.yml | 3 +- internal/graphapi/contact_test.go | 15 +++- internal/httpserve/handlers/oauth_login.go | 2 +- 8 files changed, 81 insertions(+), 29 deletions(-) diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index 78f214bcfa..85b0caecd9 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -9,7 +9,7 @@ permissions: contents: write jobs: ldflags_args: - runs-on: ubuntu-latest + runs-on: self-hosted outputs: commit-date: ${{ steps.ldflags.outputs.commit-date }} commit: ${{ steps.ldflags.outputs.commit }} @@ -35,7 +35,7 @@ jobs: permissions: contents: write # To add assets to a release. id-token: write # To do keyless signing with cosign - runs-on: ubuntu-latest + runs-on: self-hosted steps: - name: Checkout uses: actions/checkout@v6 diff --git a/config/.env.example b/config/.env.example index 38b94e7cf1..fa5f4b8b86 100644 --- a/config/.env.example +++ b/config/.env.example @@ -107,7 +107,7 @@ CORE_AUTH_TOKEN_APITOKENS_PREFIX="" CORE_AUTH_TOKEN_ASSESSMENTACCESSDURATION="1h" CORE_AUTH_TOKEN_TRUSTCENTERNDAREQUESTACCESSDURATION="1h" CORE_AUTH_SUPPORTEDPROVIDERS="" -CORE_AUTH_PROVIDERS_REDIRECTURL="http://localhost:3001/api/auth/callback/theopenlane" +CORE_AUTH_PROVIDERS_REDIRECTURL="http://localhost:3001/login/sso" CORE_AUTH_PROVIDERS_GITHUB_CLIENTID="" CORE_AUTH_PROVIDERS_GITHUB_CLIENTSECRET="" CORE_AUTH_PROVIDERS_GITHUB_CLIENTENDPOINT="" diff --git a/config/config.example.yaml b/config/config.example.yaml index 4243577ae7..1a2c0469ed 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -13,7 +13,7 @@ auth: clientsecret: "" redirecturl: /v1/google/callback scopes: [] - redirecturl: http://localhost:3001/api/auth/callback/theopenlane + redirecturl: http://localhost:3001/login/sso webauthn: debug: false displayname: "" diff --git a/config/helm-values.yaml b/config/helm-values.yaml index 8a68132b23..8bcb712c6b 100644 --- a/config/helm-values.yaml +++ b/config/helm-values.yaml @@ -253,7 +253,7 @@ coreConfiguration: # -- Providers contains supported oauth2 providers configuration providers: # -- RedirectURL is the URL that the OAuth2 client will redirect to after authentication is complete - redirecturl: "http://localhost:3001/api/auth/callback/theopenlane" # @schema type:string; default:http://localhost:3001/api/auth/callback/theopenlane + redirecturl: "http://localhost:3001/login/sso" # @schema type:string; default:http://localhost:3001/login/sso # -- Github contains the configuration settings for the Github Oauth Provider github: clientid: "" # @schema type:string diff --git a/db/MIGRATION.md b/db/MIGRATION.md index f0a46ddbd5..e77358709e 100644 --- a/db/MIGRATION.md +++ b/db/MIGRATION.md @@ -17,27 +17,28 @@ All tuples where: - `_user LIKE 'organization:%'` - `object_type != 'file'` (files use a separate ownership model) -### Steps +--- -**1. Preview the rows that will be migrated** +## Pre-release steps + +**1. Preview what will be migrated** ```sql -SELECT - store, - object_type, - object_id, - 'parent_context' AS relation, - _user, - user_type +SELECT object_type, COUNT(*) AS row_count FROM tuple WHERE relation = 'parent' AND _user LIKE 'organization:%' - AND object_type != 'file'; + AND object_type != 'file' +GROUP BY object_type +ORDER BY object_type; ``` -**2. Run the migration** +**2. Record the migration start time, then run the migration** ```sql +-- record this value; you will need it for rollback if something goes wrong +SELECT NOW() AS migration_start; + INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at) SELECT store, @@ -46,7 +47,7 @@ SELECT 'parent_context', _user, user_type, - md5(store || object_type || object_id || 'parent_context' || _user), + generate_ulid(), NOW() FROM tuple WHERE relation = 'parent' @@ -55,14 +56,44 @@ WHERE relation = 'parent' ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING; ``` -The `ulid` is derived deterministically from the natural key so the insert is idempotent — safe to re-run. +**3. Verify row counts match step 1** + +```sql +SELECT object_type, COUNT(*) AS migrated +FROM tuple +WHERE relation = 'parent_context' + AND _user LIKE 'organization:%' +GROUP BY object_type +ORDER BY object_type; +``` + +**If something looks wrong — rollback before releasing** -**3. Verify** +Substitute `$migration_start` with the timestamp recorded in step 2. -Spot-check that `parent_context` rows now exist for the same objects that had `parent` rows: +```sql +DELETE FROM tuple +WHERE relation = 'parent_context' + AND _user LIKE 'organization:%' + AND inserted_at >= '$migration_start'; +``` + +--- + +## Release + +Deploy the updated FGA model after the tuples are written. Deploying the model before the migration means objects will temporarily lose org-context permissions. + +--- + +## Post-release steps + +**4. Verify the new model is using `parent_context`** + +Spot-check a known object in a known org and confirm permissions resolve correctly. Then confirm the counts are still what you expect: ```sql -SELECT object_type, COUNT(*) +SELECT object_type, COUNT(*) AS migrated FROM tuple WHERE relation = 'parent_context' AND _user LIKE 'organization:%' @@ -70,17 +101,24 @@ GROUP BY object_type ORDER BY object_type; ``` -**4. Deploy the updated FGA model** +**5. Clean up old `parent` tuples** -The new model must be deployed after the tuples are written. Deploying the model before the migration means objects will temporarily lose org-context permissions. +Once the new model is live and verified, the old `parent` + `organization:*` tuples are no longer read by the model and can be deleted: -**5. (Optional) Clean up old `parent` tuples** +```sql +DELETE FROM tuple +WHERE relation = 'parent' + AND _user LIKE 'organization:%' + AND object_type != 'file'; +``` -Once the new model is live and verified, the old `parent` + `organization:*` tuples are no longer used and can be deleted: +Confirm the expected number of rows were removed: ```sql -DELETE FROM tuple +SELECT COUNT(*) +FROM tuple WHERE relation = 'parent' AND _user LIKE 'organization:%' AND object_type != 'file'; +-- should return 0 ``` diff --git a/docker/docker-compose-fga.yml b/docker/docker-compose-fga.yml index 4e85d1837b..b317f43c25 100644 --- a/docker/docker-compose-fga.yml +++ b/docker/docker-compose-fga.yml @@ -23,7 +23,6 @@ services: - OPENFGA_LOG_FORMAT=json - OPENFGA_DATASTORE_MAX_OPEN_CONNS=40 #see postgres container - OPENFGA_DATASTORE_MAX_IDLE_CONNS=40 - - OPENFGA_PLAYGROUND_ENABLED=true - OPENFGA_LIST_USERS_MAX_RESULTS=1000 - OPENFGA_LIST_OBJECTS_MAX_RESULTS=1000 - OPENFGA_MAX_CHECKS_PER_BATCH_CHECK=500 @@ -35,6 +34,8 @@ services: - --datastore-metrics-enabled - --metrics-enable-rpc-histograms - --authn-method=none + - --playground-enabled + - --playground-addr=0.0.0.0:3000 healthcheck: test: ["CMD", "/usr/local/bin/grpc_health_probe", "-addr=openfga:8081"] interval: 5s diff --git a/internal/graphapi/contact_test.go b/internal/graphapi/contact_test.go index bd460a89d0..213f0ed1e7 100644 --- a/internal/graphapi/contact_test.go +++ b/internal/graphapi/contact_test.go @@ -197,6 +197,10 @@ func TestQueryContacts(t *testing.T) { } func TestMutationCreateContact(t *testing.T) { + // test scopes return error, this is also to test that write -> gives read + apiClientNoContactScope := setupAPIToken(sharedTestUser1.UserCtx, t, []string{"control:write"}) + apiClientWithSpecificScope := setupAPIToken(sharedTestUser1.UserCtx, t, []string{"contact:write"}) + testCases := []struct { name string request testclient.CreateContactInput @@ -226,9 +230,18 @@ func TestMutationCreateContact(t *testing.T) { request: testclient.CreateContactInput{ FullName: lo.ToPtr("Rhaenys Targaryen"), }, - client: suite.client.apiWithToken, + client: apiClientWithSpecificScope, ctx: context.Background(), }, + { + name: "using api token without required scope", + request: testclient.CreateContactInput{ + FullName: lo.ToPtr("Rhaenys Targaryen"), + }, + client: apiClientNoContactScope, + ctx: context.Background(), + expectedErr: missingScopeErrorMsg, + }, { name: "happy path, using pat", request: testclient.CreateContactInput{ diff --git a/internal/httpserve/handlers/oauth_login.go b/internal/httpserve/handlers/oauth_login.go index dcd9388868..b612fb1048 100644 --- a/internal/httpserve/handlers/oauth_login.go +++ b/internal/httpserve/handlers/oauth_login.go @@ -32,7 +32,7 @@ import ( // OauthProviderConfig represents the configuration for OAuth providers such as Github and Google type OauthProviderConfig struct { // RedirectURL is the URL that the OAuth2 client will redirect to after authentication is complete - RedirectURL string `json:"redirecturl" koanf:"redirecturl" default:"http://localhost:3001/api/auth/callback/theopenlane"` + RedirectURL string `json:"redirecturl" koanf:"redirecturl" default:"http://localhost:3001/login/sso"` // Github contains the configuration settings for the Github Oauth Provider Github github.ProviderConfig `json:"github" koanf:"github"` // Google contains the configuration settings for the Google Oauth Provider