diff --git a/crates/cli/src/sync.rs b/crates/cli/src/sync.rs index 7676cecf3..647ef2635 100644 --- a/crates/cli/src/sync.rs +++ b/crates/cli/src/sync.rs @@ -165,11 +165,14 @@ pub async fn config_sync( } } - for provider in upstream_oauth2_config.providers { + for (index, provider) in upstream_oauth2_config.providers.into_iter().enumerate() { if !provider.enabled { continue; } + // Use the position in the config of the provider as position in the UI + let ui_order = index.try_into().unwrap_or(i32::MAX); + let _span = info_span!("provider", %provider.id).entered(); if existing_enabled_ids.contains(&provider.id) { info!("Updating provider"); @@ -293,6 +296,7 @@ pub async fn config_sync( .additional_authorization_parameters .into_iter() .collect(), + ui_order, }, ) .await?; diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/mod.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/mod.rs index b014738ae..e6dcdd22d 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/mod.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/mod.rs @@ -43,6 +43,7 @@ mod test_utils { userinfo_endpoint_override: None, jwks_uri_override: None, additional_authorization_parameters: Vec::new(), + ui_order: 0, } } } diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index a19a1ef55..dddf47fa8 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -949,6 +949,7 @@ mod tests { pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto, response_mode: None, additional_authorization_parameters: Vec::new(), + ui_order: 0, }, ) .await diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index aa2978824..e71bef309 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -436,6 +436,7 @@ mod test { pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto, response_mode: None, additional_authorization_parameters: Vec::new(), + ui_order: 0, }, ) .await @@ -476,6 +477,7 @@ mod test { pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto, response_mode: None, additional_authorization_parameters: Vec::new(), + ui_order: 1, }, ) .await diff --git a/crates/storage-pg/.sqlx/query-72de26d5e3c56f4b0658685a95b45b647bb6637e55b662a5a548aa3308c62a8a.json b/crates/storage-pg/.sqlx/query-72de26d5e3c56f4b0658685a95b45b647bb6637e55b662a5a548aa3308c62a8a.json new file mode 100644 index 000000000..7ab023046 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-72de26d5e3c56f4b0658685a95b45b647bb6637e55b662a5a548aa3308c62a8a.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters,\n ui_order,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,\n $12, $13, $14, $15, $16, $17, $18, $19, $20,\n $21, $22, $23)\n ON CONFLICT (upstream_oauth_provider_id)\n DO UPDATE\n SET\n issuer = EXCLUDED.issuer,\n human_name = EXCLUDED.human_name,\n brand_name = EXCLUDED.brand_name,\n scope = EXCLUDED.scope,\n token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method,\n token_endpoint_signing_alg = EXCLUDED.token_endpoint_signing_alg,\n id_token_signed_response_alg = EXCLUDED.id_token_signed_response_alg,\n fetch_userinfo = EXCLUDED.fetch_userinfo,\n userinfo_signed_response_alg = EXCLUDED.userinfo_signed_response_alg,\n disabled_at = NULL,\n client_id = EXCLUDED.client_id,\n encrypted_client_secret = EXCLUDED.encrypted_client_secret,\n claims_imports = EXCLUDED.claims_imports,\n authorization_endpoint_override = EXCLUDED.authorization_endpoint_override,\n token_endpoint_override = EXCLUDED.token_endpoint_override,\n userinfo_endpoint_override = EXCLUDED.userinfo_endpoint_override,\n jwks_uri_override = EXCLUDED.jwks_uri_override,\n discovery_mode = EXCLUDED.discovery_mode,\n pkce_mode = EXCLUDED.pkce_mode,\n response_mode = EXCLUDED.response_mode,\n additional_parameters = EXCLUDED.additional_parameters,\n ui_order = EXCLUDED.ui_order\n RETURNING created_at\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Bool", + "Text", + "Text", + "Text", + "Jsonb", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Jsonb", + "Int4", + "Timestamptz" + ] + }, + "nullable": [ + false + ] + }, + "hash": "72de26d5e3c56f4b0658685a95b45b647bb6637e55b662a5a548aa3308c62a8a" +} diff --git a/crates/storage-pg/.sqlx/query-99f2a0b53e08d23408dc2837d32d734c8a0e706662e72f3b2585b0c38f42c063.json b/crates/storage-pg/.sqlx/query-99f2a0b53e08d23408dc2837d32d734c8a0e706662e72f3b2585b0c38f42c063.json deleted file mode 100644 index e24c20364..000000000 --- a/crates/storage-pg/.sqlx/query-99f2a0b53e08d23408dc2837d32d734c8a0e706662e72f3b2585b0c38f42c063.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,\n $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)\n ON CONFLICT (upstream_oauth_provider_id)\n DO UPDATE\n SET\n issuer = EXCLUDED.issuer,\n human_name = EXCLUDED.human_name,\n brand_name = EXCLUDED.brand_name,\n scope = EXCLUDED.scope,\n token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method,\n token_endpoint_signing_alg = EXCLUDED.token_endpoint_signing_alg,\n id_token_signed_response_alg = EXCLUDED.id_token_signed_response_alg,\n fetch_userinfo = EXCLUDED.fetch_userinfo,\n userinfo_signed_response_alg = EXCLUDED.userinfo_signed_response_alg,\n disabled_at = NULL,\n client_id = EXCLUDED.client_id,\n encrypted_client_secret = EXCLUDED.encrypted_client_secret,\n claims_imports = EXCLUDED.claims_imports,\n authorization_endpoint_override = EXCLUDED.authorization_endpoint_override,\n token_endpoint_override = EXCLUDED.token_endpoint_override,\n userinfo_endpoint_override = EXCLUDED.userinfo_endpoint_override,\n jwks_uri_override = EXCLUDED.jwks_uri_override,\n discovery_mode = EXCLUDED.discovery_mode,\n pkce_mode = EXCLUDED.pkce_mode,\n response_mode = EXCLUDED.response_mode,\n additional_parameters = EXCLUDED.additional_parameters\n RETURNING created_at\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "created_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Bool", - "Text", - "Text", - "Text", - "Jsonb", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Jsonb", - "Timestamptz" - ] - }, - "nullable": [ - false - ] - }, - "hash": "99f2a0b53e08d23408dc2837d32d734c8a0e706662e72f3b2585b0c38f42c063" -} diff --git a/crates/storage-pg/.sqlx/query-27d6f228a9a608b5d03d30cb4074be94dc893df9107e982583aa954b5067dfd1.json b/crates/storage-pg/.sqlx/query-c1e55ffd09181c0d8ddd0df2843690aeae4a20329045ab23639181a0d0903178.json similarity index 95% rename from crates/storage-pg/.sqlx/query-27d6f228a9a608b5d03d30cb4074be94dc893df9107e982583aa954b5067dfd1.json rename to crates/storage-pg/.sqlx/query-c1e55ffd09181c0d8ddd0df2843690aeae4a20329045ab23639181a0d0903178.json index 938cab2bb..b929df201 100644 --- a/crates/storage-pg/.sqlx/query-27d6f228a9a608b5d03d30cb4074be94dc893df9107e982583aa954b5067dfd1.json +++ b/crates/storage-pg/.sqlx/query-c1e55ffd09181c0d8ddd0df2843690aeae4a20329045ab23639181a0d0903178.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n created_at,\n disabled_at,\n claims_imports as \"claims_imports: Json\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters as \"additional_parameters: Json>\"\n FROM upstream_oauth_providers\n WHERE disabled_at IS NULL\n ", + "query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n created_at,\n disabled_at,\n claims_imports as \"claims_imports: Json\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters as \"additional_parameters: Json>\"\n FROM upstream_oauth_providers\n WHERE disabled_at IS NULL\n ORDER BY ui_order ASC, upstream_oauth_provider_id ASC\n ", "describe": { "columns": [ { @@ -148,5 +148,5 @@ true ] }, - "hash": "27d6f228a9a608b5d03d30cb4074be94dc893df9107e982583aa954b5067dfd1" + "hash": "c1e55ffd09181c0d8ddd0df2843690aeae4a20329045ab23639181a0d0903178" } diff --git a/crates/storage-pg/migrations/20250312094013_upstream_oauth2_providers_order.sql b/crates/storage-pg/migrations/20250312094013_upstream_oauth2_providers_order.sql new file mode 100644 index 000000000..4cf422d7d --- /dev/null +++ b/crates/storage-pg/migrations/20250312094013_upstream_oauth2_providers_order.sql @@ -0,0 +1,9 @@ +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + +-- Adds a column to track the 'UI order' of the upstream OAuth2 providers, so +-- that they can be consistently displayed in the UI +ALTER TABLE upstream_oauth_providers + ADD COLUMN ui_order INTEGER NOT NULL DEFAULT 0; diff --git a/crates/storage-pg/src/upstream_oauth2/mod.rs b/crates/storage-pg/src/upstream_oauth2/mod.rs index 2d0030426..d802e9bdb 100644 --- a/crates/storage-pg/src/upstream_oauth2/mod.rs +++ b/crates/storage-pg/src/upstream_oauth2/mod.rs @@ -76,6 +76,7 @@ mod tests { pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto, response_mode: None, additional_authorization_parameters: Vec::new(), + ui_order: 0, }, ) .await @@ -322,6 +323,7 @@ mod tests { pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto, response_mode: None, additional_authorization_parameters: Vec::new(), + ui_order: 0, }, ) .await diff --git a/crates/storage-pg/src/upstream_oauth2/provider.rs b/crates/storage-pg/src/upstream_oauth2/provider.rs index b65c05e78..2e5f7233f 100644 --- a/crates/storage-pg/src/upstream_oauth2/provider.rs +++ b/crates/storage-pg/src/upstream_oauth2/provider.rs @@ -517,9 +517,11 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { pkce_mode, response_mode, additional_parameters, + ui_order, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, - $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) + $12, $13, $14, $15, $16, $17, $18, $19, $20, + $21, $22, $23) ON CONFLICT (upstream_oauth_provider_id) DO UPDATE SET @@ -543,7 +545,8 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { discovery_mode = EXCLUDED.discovery_mode, pkce_mode = EXCLUDED.pkce_mode, response_mode = EXCLUDED.response_mode, - additional_parameters = EXCLUDED.additional_parameters + additional_parameters = EXCLUDED.additional_parameters, + ui_order = EXCLUDED.ui_order RETURNING created_at "#, Uuid::from(id), @@ -582,6 +585,7 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { params.pkce_mode.as_str(), params.response_mode.as_ref().map(ToString::to_string), Json(¶ms.additional_authorization_parameters) as _, + params.ui_order, created_at, ) .traced() @@ -917,6 +921,7 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { additional_parameters as "additional_parameters: Json>" FROM upstream_oauth_providers WHERE disabled_at IS NULL + ORDER BY ui_order ASC, upstream_oauth_provider_id ASC "#, ) .traced() diff --git a/crates/storage/src/upstream_oauth2/provider.rs b/crates/storage/src/upstream_oauth2/provider.rs index 47a523df4..673050a8f 100644 --- a/crates/storage/src/upstream_oauth2/provider.rs +++ b/crates/storage/src/upstream_oauth2/provider.rs @@ -95,6 +95,9 @@ pub struct UpstreamOAuthProviderParams { /// Additional parameters to include in the authorization request pub additional_authorization_parameters: Vec<(String, String)>, + + /// The position of the provider in the UI + pub ui_order: i32, } /// Filter parameters for listing upstream OAuth 2.0 providers diff --git a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_upstream_provider_link.snap b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_upstream_provider_link.snap index a28548e2b..1fbf6a100 100644 --- a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_upstream_provider_link.snap +++ b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_upstream_provider_link.snap @@ -30,6 +30,7 @@ upstream_oauth_providers: token_endpoint_auth_method: client_secret_basic token_endpoint_override: ~ token_endpoint_signing_alg: ~ + ui_order: "0" upstream_oauth_provider_id: 00000000-0000-0000-0000-000000000004 userinfo_endpoint_override: ~ userinfo_signed_response_alg: ~