mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat: constrain the OAuth2 client type column (#27931)
Extracted from #27873 so the schema change can be reviewed for migration safety on its own. #27873 will rebase onto this. `client_type` decides whether the token endpoint validates a client secret at all, and the column accepts any text: nullable, no `CHECK`, no enum. No Go path can write a bad value today, and `IsPublic` fails closed on anything unrecognized, so the read side is safe. What the schema still permits is the problem: a future migration writing `'public'` onto a row that holds a secret turns off client authentication for that app with nothing to catch it, no constraint, no log, no audit entry, no test. `000565` adds `CHECK (client_type IN ('confidential', 'public'))` and `NOT NULL`. The `UPDATE` ahead of it should touch zero rows, since migration `000344` added the column with a default of `'confidential'` and backfilled with `COALESCE`; it is there so `SET NOT NULL` cannot fail on an unexpected row. Both `ALTER`s take `ACCESS EXCLUSIVE` and scan a table holding one row per registered OAuth2 client, so the lock is brief. ## The second migration, and why it aligns the way it does Two columns describe the same fact and can currently contradict each other. `token_endpoint_auth_method` is the client's own declaration: registered client metadata under RFC 7591 §2, where `"none"` is defined to mean the client is public and has no secret. `client_type` is Coder's derived copy, and it is what the token endpoint enforces on. RFC 7591 defines no `client_type` metadata field; the column exists only as a denormalization. Registration used to persist the declaration verbatim while hardcoding `client_type` to `'confidential'`, so rows exist declaring `"none"` on a client stored confidential that was issued, and still requires, a real secret. A client that reads its own metadata and believes it is public will drop that secret and stop being able to exchange codes. `000566` aligns the declaration to what is enforced, not the reverse. Deriving enforcement from the declaration would reclassify every such client as public and stop requiring the secret it holds, which is a silent authentication downgrade. The down migration is deliberately empty: the previous values are not recorded, and restoring them would only reinstate metadata that tells a client to authenticate in a way the server rejects. ## Application changes `SET NOT NULL` changes the generated field from `sql.NullString` to `string`, so the three write sites are updated to match. That is the entire application diff and no behavior depends on it. Refs https://linear.app/codercom/issue/ENG-3029/oauth2-support-public-client --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2f34e1abd0
commit
16c58770f8
Generated
+1
@@ -44,6 +44,7 @@ const (
|
||||
CheckMcpServerConfigsAuthTypeCheck CheckConstraint = "mcp_server_configs_auth_type_check" // mcp_server_configs
|
||||
CheckMcpServerConfigsAvailabilityCheck CheckConstraint = "mcp_server_configs_availability_check" // mcp_server_configs
|
||||
CheckMcpServerConfigsTransportCheck CheckConstraint = "mcp_server_configs_transport_check" // mcp_server_configs
|
||||
CheckOauth2ProviderAppsClientTypeCheck CheckConstraint = "oauth2_provider_apps_client_type_check" // oauth2_provider_apps
|
||||
CheckMaxProvisionerLogsLength CheckConstraint = "max_provisioner_logs_length" // provisioner_jobs
|
||||
CheckNatsPortValidTcp CheckConstraint = "nats_port_valid_tcp" // replicas
|
||||
CheckMaxLogsLength CheckConstraint = "max_logs_length" // workspace_agents
|
||||
|
||||
@@ -5822,7 +5822,11 @@ func (s *MethodTestSuite) TestOAuth2ProviderApps() {
|
||||
})
|
||||
}))
|
||||
s.Run("InsertOAuth2ProviderApp", s.Subtest(func(db database.Store, check *expects) {
|
||||
check.Args(database.InsertOAuth2ProviderAppParams{}).Asserts(rbac.ResourceOauth2App, policy.ActionCreate)
|
||||
// client_type is NOT NULL with a CHECK for the two canonical values, and
|
||||
// the insert always sends the column, so the zero value cannot be used.
|
||||
check.Args(database.InsertOAuth2ProviderAppParams{
|
||||
ClientType: "confidential",
|
||||
}).Asserts(rbac.ResourceOauth2App, policy.ActionCreate)
|
||||
}))
|
||||
s.Run("UpdateOAuth2ProviderAppByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
dbtestutil.DisableForeignKeysAndTriggers(s.T(), db)
|
||||
|
||||
@@ -1733,7 +1733,7 @@ func OAuth2ProviderApp(t testing.TB, db database.Store, seed database.OAuth2Prov
|
||||
Icon: takeFirst(seed.Icon, ""),
|
||||
CallbackURL: takeFirst(seed.CallbackURL, "http://localhost"),
|
||||
RedirectUris: takeFirstSlice(seed.RedirectUris, []string{}),
|
||||
ClientType: takeFirst(seed.ClientType, sql.NullString{String: "confidential", Valid: true}),
|
||||
ClientType: takeFirst(seed.ClientType, "confidential"),
|
||||
DynamicallyRegistered: takeFirst(seed.DynamicallyRegistered, sql.NullBool{Bool: false, Valid: true}),
|
||||
ClientIDIssuedAt: takeFirst(seed.ClientIDIssuedAt, sql.NullTime{}),
|
||||
ClientSecretExpiresAt: takeFirst(seed.ClientSecretExpiresAt, sql.NullTime{}),
|
||||
|
||||
Generated
+3
-2
@@ -2652,7 +2652,7 @@ CREATE TABLE oauth2_provider_apps (
|
||||
icon character varying(256) NOT NULL,
|
||||
callback_url text NOT NULL,
|
||||
redirect_uris text[],
|
||||
client_type text DEFAULT 'confidential'::text,
|
||||
client_type text DEFAULT 'confidential'::text NOT NULL,
|
||||
dynamically_registered boolean DEFAULT false,
|
||||
client_id_issued_at timestamp with time zone DEFAULT now(),
|
||||
client_secret_expires_at timestamp with time zone,
|
||||
@@ -2670,7 +2670,8 @@ CREATE TABLE oauth2_provider_apps (
|
||||
software_id text,
|
||||
software_version text,
|
||||
registration_access_token bytea,
|
||||
registration_client_uri text
|
||||
registration_client_uri text,
|
||||
CONSTRAINT oauth2_provider_apps_client_type_check CHECK ((client_type = ANY (ARRAY['confidential'::text, 'public'::text])))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE oauth2_provider_apps IS 'A table used to configure apps that can use Coder as an OAuth2 provider, the reverse of what we are calling external authentication.';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE oauth2_provider_apps
|
||||
ALTER COLUMN client_type DROP NOT NULL;
|
||||
|
||||
ALTER TABLE oauth2_provider_apps
|
||||
DROP CONSTRAINT oauth2_provider_apps_client_type_check;
|
||||
@@ -0,0 +1,19 @@
|
||||
-- client_type decides whether the token endpoint validates a client secret at
|
||||
-- all. Constrain it at the schema level: no Go path can write a bad value
|
||||
-- today, but a future migration writing 'public' onto a row that holds a secret
|
||||
-- would turn off client authentication for that app with nothing to catch it.
|
||||
--
|
||||
-- A non-NULL value outside the two canonical strings fails ADD CONSTRAINT and,
|
||||
-- because all migrations share one transaction, blocks the upgrade. That is
|
||||
-- deliberate: coercing an unexplained value would hide whatever wrote it.
|
||||
--
|
||||
-- This should touch zero rows: migration 000344 added the column with a default
|
||||
-- of 'confidential' and backfilled existing rows with COALESCE.
|
||||
UPDATE oauth2_provider_apps SET client_type = 'confidential' WHERE client_type IS NULL;
|
||||
|
||||
ALTER TABLE oauth2_provider_apps
|
||||
ADD CONSTRAINT oauth2_provider_apps_client_type_check
|
||||
CHECK (client_type IN ('confidential', 'public'));
|
||||
|
||||
ALTER TABLE oauth2_provider_apps
|
||||
ALTER COLUMN client_type SET NOT NULL;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Deliberately a no-op.
|
||||
--
|
||||
-- The up migration does not record which rows it touched, so the previous
|
||||
-- values cannot be restored, and restoring them would only reinstate metadata
|
||||
-- that tells a client to authenticate in a way the server rejects. The schema
|
||||
-- is unchanged either way, so rolling back past this migration needs no
|
||||
-- structural work.
|
||||
@@ -0,0 +1,31 @@
|
||||
-- token_endpoint_auth_method is the client's declared auth method (RFC 7591
|
||||
-- section 2); client_type is Coder's own field, and it is what the token
|
||||
-- endpoint actually enforces. The two can disagree: registration used to
|
||||
-- persist a client's "none" declaration while hardcoding client_type to
|
||||
-- confidential, so some clients declare themselves public while still
|
||||
-- holding, and needing, a real secret.
|
||||
--
|
||||
-- Align the declaration to what is enforced, not the reverse: deriving
|
||||
-- enforcement from the declaration would silently stop requiring the secret
|
||||
-- those clients already hold.
|
||||
--
|
||||
-- Each branch repairs anything outside the valid set for its client_type,
|
||||
-- not just the one bad value seen so far, so a stray '' or unrecognized
|
||||
-- method can't slip through and later pass a client_type/auth_method
|
||||
-- consistency check unnoticed.
|
||||
--
|
||||
-- The IS NULL arm matters: NULL NOT IN (...) evaluates to NULL, and WHERE
|
||||
-- only admits true, so without it NULL rows would stop being repaired.
|
||||
UPDATE oauth2_provider_apps
|
||||
SET token_endpoint_auth_method = 'client_secret_basic' -- the RFC 7591 section 2 default for a client with a secret
|
||||
WHERE client_type = 'confidential'
|
||||
AND (token_endpoint_auth_method IS NULL
|
||||
OR token_endpoint_auth_method NOT IN ('client_secret_basic', 'client_secret_post'));
|
||||
|
||||
-- The mirror case is not reachable through any current code path, since a public
|
||||
-- client is only ever created by requesting 'none'. Included so the invariant
|
||||
-- holds for the whole table rather than for the half that had a known bug.
|
||||
UPDATE oauth2_provider_apps
|
||||
SET token_endpoint_auth_method = 'none'
|
||||
WHERE client_type = 'public'
|
||||
AND (token_endpoint_auth_method IS NULL OR token_endpoint_auth_method <> 'none');
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/database/migrations"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
@@ -2739,3 +2740,255 @@ func TestMigration000562OAuth2PublicClientTokensBackfill(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "YES", isNullable, "app_secret_id should be nullable after the migration")
|
||||
}
|
||||
|
||||
// setupMigration000565Apps steps to the migration just before 000565 and seeds
|
||||
// one oauth2_provider_apps row per token_endpoint_auth_method shape that 000566
|
||||
// repairs or preserves. 000565's NULL client_type case is seeded in its own
|
||||
// test, since it is about the column this helper always populates. Returns the
|
||||
// app IDs keyed by the shape they were seeded with.
|
||||
func setupMigration000565Apps(t *testing.T) (*sql.DB, context.Context, map[string]uuid.UUID) {
|
||||
t.Helper()
|
||||
|
||||
const priorMigrationVersion = 564
|
||||
|
||||
sqlDB := testSQLDB(t)
|
||||
next, err := migrations.Stepper(sqlDB)
|
||||
require.NoError(t, err)
|
||||
for {
|
||||
version, more, err := next()
|
||||
require.NoError(t, err)
|
||||
if !more {
|
||||
t.Fatalf("migration %d not found", priorMigrationVersion)
|
||||
}
|
||||
if version == priorMigrationVersion {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
|
||||
// "legacy" is the row the known bug produced: the client asked for "none",
|
||||
// which RFC 7591 section 2 defines as public, but client_type was hardcoded
|
||||
// confidential and a secret was issued anyway.
|
||||
ids := map[string]uuid.UUID{
|
||||
"legacy": uuid.New(), // confidential + none -> auth method must be repaired
|
||||
"nullMethod": uuid.New(), // confidential + NULL -> auth method must be repaired
|
||||
"publicMismatched": uuid.New(), // public + client_secret_basic -> auth method must be repaired
|
||||
"confidentialBasic": uuid.New(), // already consistent -> must be left alone
|
||||
"confidentialPost": uuid.New(), // already consistent, and post must not be flattened to basic
|
||||
"publicNone": uuid.New(), // already consistent -> must be left alone
|
||||
"confidentialEmpty": uuid.New(), // confidential + '' -> repaired only by the widened predicate
|
||||
"confidentialJunk": uuid.New(), // confidential + unknown -> repaired only by the widened predicate
|
||||
"publicNull": uuid.New(), // public + NULL -> the second UPDATE's IS NULL arm
|
||||
}
|
||||
|
||||
seed := func(id uuid.UUID, name, clientType string, authMethod *string) {
|
||||
t.Helper()
|
||||
_, err := sqlDB.ExecContext(ctx, `
|
||||
INSERT INTO oauth2_provider_apps
|
||||
(id, created_at, updated_at, name, icon, callback_url, client_type, token_endpoint_auth_method)
|
||||
VALUES ($1, $2, $2, $3, '', 'http://localhost/callback', $4, $5)
|
||||
`, id, now, name, clientType, authMethod)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
seed(ids["legacy"], "test-565-legacy", "confidential", ptr.Ref("none"))
|
||||
seed(ids["nullMethod"], "test-565-null", "confidential", nil)
|
||||
seed(ids["publicMismatched"], "test-565-public-mismatch", "public", ptr.Ref("client_secret_basic"))
|
||||
seed(ids["confidentialBasic"], "test-565-basic", "confidential", ptr.Ref("client_secret_basic"))
|
||||
seed(ids["confidentialPost"], "test-565-post", "confidential", ptr.Ref("client_secret_post"))
|
||||
seed(ids["publicNone"], "test-565-public", "public", ptr.Ref("none"))
|
||||
// Neither of these is producible by today's write path: ApplyDefaults maps
|
||||
// "" to client_secret_basic and Valid() rejects unknown methods. They stand
|
||||
// in for history the squashed log cannot rule out, and both would satisfy
|
||||
// the eventual cross-column constraint while remaining unusable.
|
||||
seed(ids["confidentialEmpty"], "test-565-empty", "confidential", ptr.Ref(""))
|
||||
seed(ids["confidentialJunk"], "test-565-junk", "confidential", ptr.Ref("client_secret_jwt"))
|
||||
seed(ids["publicNull"], "test-565-public-null", "public", nil)
|
||||
|
||||
return sqlDB, ctx, ids
|
||||
}
|
||||
|
||||
// TestMigration000565OAuth2ClientTypeConstraint covers the constraint migration:
|
||||
// a NULL client_type is backfilled, the column becomes NOT NULL, and the CHECK
|
||||
// rejects any value other than the two canonical ones.
|
||||
func TestMigration000565OAuth2ClientTypeConstraint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqlDB, ctx, _ := setupMigration000565Apps(t)
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
|
||||
// A NULL client_type must survive the migration as 'confidential', the
|
||||
// fail-closed direction, rather than blocking SET NOT NULL.
|
||||
nullTypeID := uuid.New()
|
||||
_, err := sqlDB.ExecContext(ctx, `
|
||||
INSERT INTO oauth2_provider_apps
|
||||
(id, created_at, updated_at, name, icon, callback_url, client_type)
|
||||
VALUES ($1, $2, $2, 'test-565-nulltype', '', 'http://localhost/callback', NULL)
|
||||
`, nullTypeID, now)
|
||||
require.NoError(t, err)
|
||||
|
||||
migrationSQL, err := os.ReadFile("000565_oauth2_client_type_constraint.up.sql")
|
||||
require.NoError(t, err)
|
||||
_, err = sqlDB.ExecContext(ctx, string(migrationSQL))
|
||||
require.NoError(t, err)
|
||||
|
||||
var backfilled string
|
||||
err = sqlDB.QueryRowContext(ctx,
|
||||
`SELECT client_type FROM oauth2_provider_apps WHERE id = $1`, nullTypeID,
|
||||
).Scan(&backfilled)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "confidential", backfilled,
|
||||
"a NULL client_type must read as confidential, the direction that keeps requiring a secret")
|
||||
|
||||
var isNullable string
|
||||
err = sqlDB.QueryRowContext(ctx, `
|
||||
SELECT is_nullable FROM information_schema.columns
|
||||
WHERE table_name = 'oauth2_provider_apps' AND column_name = 'client_type'
|
||||
`).Scan(&isNullable)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "NO", isNullable, "client_type should be NOT NULL after the migration")
|
||||
|
||||
// Prove the CHECK rejects rather than trusting that it exists.
|
||||
for _, badValue := range []string{"Public", "PUBLIC", "public ", "bogus", ""} {
|
||||
_, err = sqlDB.ExecContext(ctx, `
|
||||
INSERT INTO oauth2_provider_apps
|
||||
(id, created_at, updated_at, name, icon, callback_url, client_type)
|
||||
VALUES ($1, $2, $2, $3, '', 'http://localhost/callback', $4)
|
||||
`, uuid.New(), now, "test-565-bad-"+badValue, badValue)
|
||||
require.Error(t, err, "client_type %q must be rejected by the CHECK constraint", badValue)
|
||||
require.True(t, database.IsCheckViolation(err, database.CheckOauth2ProviderAppsClientTypeCheck),
|
||||
"client_type %q must fail the check constraint specifically", badValue)
|
||||
}
|
||||
|
||||
_, err = sqlDB.ExecContext(ctx, `
|
||||
INSERT INTO oauth2_provider_apps
|
||||
(id, created_at, updated_at, name, icon, callback_url, client_type)
|
||||
VALUES ($1, $2, $2, 'test-565-null-after', '', 'http://localhost/callback', NULL)
|
||||
`, uuid.New(), now)
|
||||
require.ErrorContains(t, err, "not-null")
|
||||
|
||||
for _, goodValue := range []string{"confidential", "public"} {
|
||||
_, err = sqlDB.ExecContext(ctx, `
|
||||
INSERT INTO oauth2_provider_apps
|
||||
(id, created_at, updated_at, name, icon, callback_url, client_type)
|
||||
VALUES ($1, $2, $2, $3, '', 'http://localhost/callback', $4)
|
||||
`, uuid.New(), now, "test-565-good-"+goodValue, goodValue)
|
||||
require.NoError(t, err, "client_type %q must remain valid", goodValue)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigration000566OAuth2AuthMethodBackfill covers the repair the backfill
|
||||
// exists for, which the testdata/fixtures run does not reach: its only
|
||||
// oauth2_provider_apps row seeds no token_endpoint_auth_method at all, so the
|
||||
// '= none' branch, the one that fixes the actual bug, matches zero rows in CI.
|
||||
func TestMigration000566OAuth2AuthMethodBackfill(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqlDB, ctx, ids := setupMigration000565Apps(t)
|
||||
|
||||
// 000566 runs after 000565, so apply both in order.
|
||||
for _, name := range []string{
|
||||
"000565_oauth2_client_type_constraint.up.sql",
|
||||
"000566_oauth2_auth_method_backfill.up.sql",
|
||||
} {
|
||||
migrationSQL, err := os.ReadFile(name)
|
||||
require.NoError(t, err)
|
||||
_, err = sqlDB.ExecContext(ctx, string(migrationSQL))
|
||||
require.NoError(t, err, "applying %s", name)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
key string
|
||||
wantMethod string
|
||||
reason string
|
||||
}{
|
||||
{
|
||||
key: "legacy",
|
||||
wantMethod: "client_secret_basic",
|
||||
reason: "a confidential client declaring \"none\" is the bug this migration repairs",
|
||||
},
|
||||
{
|
||||
key: "nullMethod",
|
||||
wantMethod: "client_secret_basic",
|
||||
reason: "a confidential client with no declaration gets the RFC 7591 default",
|
||||
},
|
||||
{
|
||||
key: "publicMismatched",
|
||||
wantMethod: "none",
|
||||
reason: "a public client cannot declare a secret-based method",
|
||||
},
|
||||
{
|
||||
key: "confidentialBasic",
|
||||
wantMethod: "client_secret_basic",
|
||||
reason: "already consistent, must be left alone",
|
||||
},
|
||||
{
|
||||
key: "confidentialPost",
|
||||
wantMethod: "client_secret_post",
|
||||
reason: "client_secret_post is consistent with confidential and must not be flattened to basic",
|
||||
},
|
||||
{
|
||||
key: "publicNone",
|
||||
wantMethod: "none",
|
||||
reason: "already consistent, must be left alone",
|
||||
},
|
||||
{
|
||||
key: "confidentialEmpty",
|
||||
wantMethod: "client_secret_basic",
|
||||
reason: "an empty declaration is not a valid confidential method and must be repaired, not just the known 'none' case",
|
||||
},
|
||||
{
|
||||
key: "confidentialJunk",
|
||||
wantMethod: "client_secret_basic",
|
||||
reason: "an unrecognized declaration must be repaired too, so the migration is idempotent against history it cannot inspect",
|
||||
},
|
||||
{
|
||||
key: "publicNull",
|
||||
wantMethod: "none",
|
||||
reason: "exercises the second UPDATE's IS NULL arm, which no other seeded row reaches",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// A parallel subtest's own context, not the parent's. The parent's
|
||||
// deadline starts when it is created, but a parallel subtest does
|
||||
// not run until a -parallel slot frees, so it can spend most of the
|
||||
// budget queued behind other tests in the package and then fail with
|
||||
// "context deadline exceeded" on a sub-20ms read. That failure names
|
||||
// no code and gets worse as the package grows.
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
var gotMethod, gotClientType string
|
||||
err := sqlDB.QueryRowContext(ctx, `
|
||||
SELECT token_endpoint_auth_method, client_type
|
||||
FROM oauth2_provider_apps WHERE id = $1
|
||||
`, ids[tt.key]).Scan(&gotMethod, &gotClientType)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantMethod, gotMethod, tt.reason)
|
||||
})
|
||||
}
|
||||
|
||||
// IS DISTINCT FROM, not <>: with <> a NULL declaration makes the comparison
|
||||
// NULL and WHERE drops the row, so an unrepaired NULL would count as
|
||||
// consistent. The same trap awaits the permanent cross-column CHECK.
|
||||
var contradictions int
|
||||
err := sqlDB.QueryRowContext(ctx, `
|
||||
SELECT count(*) FROM oauth2_provider_apps
|
||||
WHERE (token_endpoint_auth_method = 'none') IS DISTINCT FROM (client_type = 'public')
|
||||
`).Scan(&contradictions)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, contradictions,
|
||||
"after the backfill no row may declare an auth method that contradicts its enforced client type")
|
||||
|
||||
var stillConfidential string
|
||||
err = sqlDB.QueryRowContext(ctx,
|
||||
`SELECT client_type FROM oauth2_provider_apps WHERE id = $1`, ids["legacy"],
|
||||
).Scan(&stillConfidential)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "confidential", stillConfidential,
|
||||
"the backfill aligns the declaration to what is enforced, so the enforced value must be unchanged")
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -5525,7 +5525,7 @@ type OAuth2ProviderApp struct {
|
||||
// List of valid redirect URIs for the application
|
||||
RedirectUris []string `db:"redirect_uris" json:"redirect_uris"`
|
||||
// OAuth2 client type: confidential or public
|
||||
ClientType sql.NullString `db:"client_type" json:"client_type"`
|
||||
ClientType string `db:"client_type" json:"client_type"`
|
||||
// Whether this app was created via dynamic client registration
|
||||
DynamicallyRegistered sql.NullBool `db:"dynamically_registered" json:"dynamically_registered"`
|
||||
// RFC 7591: Timestamp when client_id was issued
|
||||
|
||||
Generated
+3
-3
@@ -19246,7 +19246,7 @@ type InsertOAuth2ProviderAppParams struct {
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
CallbackURL string `db:"callback_url" json:"callback_url"`
|
||||
RedirectUris []string `db:"redirect_uris" json:"redirect_uris"`
|
||||
ClientType sql.NullString `db:"client_type" json:"client_type"`
|
||||
ClientType string `db:"client_type" json:"client_type"`
|
||||
DynamicallyRegistered sql.NullBool `db:"dynamically_registered" json:"dynamically_registered"`
|
||||
ClientIDIssuedAt sql.NullTime `db:"client_id_issued_at" json:"client_id_issued_at"`
|
||||
ClientSecretExpiresAt sql.NullTime `db:"client_secret_expires_at" json:"client_secret_expires_at"`
|
||||
@@ -19555,7 +19555,7 @@ type UpdateOAuth2ProviderAppByClientIDParams struct {
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
CallbackURL string `db:"callback_url" json:"callback_url"`
|
||||
RedirectUris []string `db:"redirect_uris" json:"redirect_uris"`
|
||||
ClientType sql.NullString `db:"client_type" json:"client_type"`
|
||||
ClientType string `db:"client_type" json:"client_type"`
|
||||
ClientSecretExpiresAt sql.NullTime `db:"client_secret_expires_at" json:"client_secret_expires_at"`
|
||||
GrantTypes []string `db:"grant_types" json:"grant_types"`
|
||||
ResponseTypes []string `db:"response_types" json:"response_types"`
|
||||
@@ -19661,7 +19661,7 @@ type UpdateOAuth2ProviderAppByIDParams struct {
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
CallbackURL string `db:"callback_url" json:"callback_url"`
|
||||
RedirectUris []string `db:"redirect_uris" json:"redirect_uris"`
|
||||
ClientType sql.NullString `db:"client_type" json:"client_type"`
|
||||
ClientType string `db:"client_type" json:"client_type"`
|
||||
DynamicallyRegistered sql.NullBool `db:"dynamically_registered" json:"dynamically_registered"`
|
||||
ClientSecretExpiresAt sql.NullTime `db:"client_secret_expires_at" json:"client_secret_expires_at"`
|
||||
GrantTypes []string `db:"grant_types" json:"grant_types"`
|
||||
|
||||
@@ -92,7 +92,7 @@ func CreateApp(db database.Store, accessURL *url.URL, auditor *audit.Auditor, lo
|
||||
Icon: req.Icon,
|
||||
CallbackURL: req.CallbackURL,
|
||||
RedirectUris: []string{},
|
||||
ClientType: sql.NullString{String: "confidential", Valid: true},
|
||||
ClientType: "confidential",
|
||||
DynamicallyRegistered: sql.NullBool{Bool: false, Valid: true},
|
||||
ClientIDIssuedAt: sql.NullTime{},
|
||||
ClientSecretExpiresAt: sql.NullTime{},
|
||||
|
||||
@@ -101,7 +101,7 @@ func CreateDynamicClientRegistration(db database.Store, accessURL *url.URL, audi
|
||||
Icon: req.LogoURI,
|
||||
CallbackURL: req.RedirectURIs[0], // Primary redirect URI
|
||||
RedirectUris: req.RedirectURIs,
|
||||
ClientType: sql.NullString{String: req.DetermineClientType(), Valid: true},
|
||||
ClientType: req.DetermineClientType(),
|
||||
DynamicallyRegistered: sql.NullBool{Bool: true, Valid: true},
|
||||
ClientIDIssuedAt: sql.NullTime{Time: now, Valid: true},
|
||||
ClientSecretExpiresAt: sql.NullTime{}, // No expiration for now
|
||||
@@ -321,7 +321,7 @@ func UpdateClientConfiguration(db database.Store, auditor *audit.Auditor, logger
|
||||
Icon: req.LogoURI,
|
||||
CallbackURL: req.RedirectURIs[0], // Primary redirect URI
|
||||
RedirectUris: req.RedirectURIs,
|
||||
ClientType: sql.NullString{String: req.DetermineClientType(), Valid: true},
|
||||
ClientType: req.DetermineClientType(),
|
||||
ClientSecretExpiresAt: sql.NullTime{}, // No expiration for now
|
||||
GrantTypes: slice.ToStrings(req.GrantTypes),
|
||||
ResponseTypes: slice.ToStrings(req.ResponseTypes),
|
||||
|
||||
Reference in New Issue
Block a user