From 075c1762e26047142b6b69264d19365f2c407e75 Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Sun, 12 Apr 2026 21:25:03 +0000 Subject: [PATCH] registry: add sqlx checks for user profile reads --- ...ec695ab1d68b38da154567aab5f897fe3bd24.json | 48 +++++++++++ ...52d762d66cffdc449ee6f73c74c1d23a17314.json | 23 +++++ crates/crank-registry/src/postgres/auth.rs | 28 ++++--- crates/crank-registry/src/postgres/mod.rs | 84 +++++++++++++++++-- 4 files changed, 166 insertions(+), 17 deletions(-) create mode 100644 .sqlx/query-0cd29169ff0987c8e32a6b2fb6cec695ab1d68b38da154567aab5f897fe3bd24.json create mode 100644 .sqlx/query-2b819bad2a2712dab1a182b89ec52d762d66cffdc449ee6f73c74c1d23a17314.json diff --git a/.sqlx/query-0cd29169ff0987c8e32a6b2fb6cec695ab1d68b38da154567aab5f897fe3bd24.json b/.sqlx/query-0cd29169ff0987c8e32a6b2fb6cec695ab1d68b38da154567aab5f897fe3bd24.json new file mode 100644 index 0000000..c44432a --- /dev/null +++ b/.sqlx/query-0cd29169ff0987c8e32a6b2fb6cec695ab1d68b38da154567aab5f897fe3bd24.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "update users\n set email = $2,\n display_name = $3\n where id = $1\n returning\n id,\n email,\n display_name,\n status,\n to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "email", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "display_name", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "created_at!", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + null + ] + }, + "hash": "0cd29169ff0987c8e32a6b2fb6cec695ab1d68b38da154567aab5f897fe3bd24" +} diff --git a/.sqlx/query-2b819bad2a2712dab1a182b89ec52d762d66cffdc449ee6f73c74c1d23a17314.json b/.sqlx/query-2b819bad2a2712dab1a182b89ec52d762d66cffdc449ee6f73c74c1d23a17314.json new file mode 100644 index 0000000..11227cc --- /dev/null +++ b/.sqlx/query-2b819bad2a2712dab1a182b89ec52d762d66cffdc449ee6f73c74c1d23a17314.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "select exists(\n select 1\n from memberships\n where user_id = $1\n and workspace_id = $2\n ) as \"allowed!\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "allowed!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "2b819bad2a2712dab1a182b89ec52d762d66cffdc449ee6f73c74c1d23a17314" +} diff --git a/crates/crank-registry/src/postgres/auth.rs b/crates/crank-registry/src/postgres/auth.rs index 8ca803b..6130908 100644 --- a/crates/crank-registry/src/postgres/auth.rs +++ b/crates/crank-registry/src/postgres/auth.rs @@ -138,7 +138,7 @@ impl PostgresRegistry { email: &str, display_name: &str, ) -> Result { - let row = sqlx::query( + let row = sqlx::query!( "update users set email = $2, display_name = $3 @@ -148,16 +148,22 @@ impl PostgresRegistry { email, display_name, status, - to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at", + to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\"", + user_id.as_str(), + email, + display_name, ) - .bind(user_id.as_str()) - .bind(email) - .bind(display_name) .fetch_one(&self.pool) .await .map_err(|error| map_user_update_error(error, user_id, email))?; - map_user(&row) + build_user( + row.id, + row.email, + row.display_name, + row.status, + row.created_at, + ) } pub async fn update_user_password( @@ -340,20 +346,20 @@ impl PostgresRegistry { user_id: &UserId, workspace_id: &WorkspaceId, ) -> Result { - let row = sqlx::query( + let row = sqlx::query!( "select exists( select 1 from memberships where user_id = $1 and workspace_id = $2 - ) as allowed", + ) as \"allowed!\"", + user_id.as_str(), + workspace_id.as_str(), ) - .bind(user_id.as_str()) - .bind(workspace_id.as_str()) .fetch_one(&self.pool) .await?; - Ok(row.try_get("allowed")?) + Ok(row.allowed) } pub async fn save_auth_profile( diff --git a/crates/crank-registry/src/postgres/mod.rs b/crates/crank-registry/src/postgres/mod.rs index d39c21e..3df1e8b 100644 --- a/crates/crank-registry/src/postgres/mod.rs +++ b/crates/crank-registry/src/postgres/mod.rs @@ -484,13 +484,19 @@ fn assert_immutable_fields( Ok(()) } -fn map_user(row: &PgRow) -> Result { +fn build_user( + id: String, + email: String, + display_name: String, + status: String, + created_at: String, +) -> Result { Ok(User { - id: UserId::new(row.try_get::("id")?), - email: row.try_get("email")?, - display_name: row.try_get("display_name")?, - status: deserialize_enum_text(&row.try_get::("status")?, "status")?, - created_at: row.try_get("created_at")?, + id: UserId::new(id), + email, + display_name, + status: deserialize_enum_text(&status, "status")?, + created_at, }) } @@ -1503,6 +1509,72 @@ mod tests { database.cleanup().await; } + #[tokio::test] + async fn manages_user_profile_and_workspace_access_reads() { + let database = TestDatabase::new().await; + let registry = database.registry().await; + let workspace = Workspace { + id: WorkspaceId::new("ws_profile_01"), + slug: "profile".to_owned(), + display_name: "Profile Workspace".to_owned(), + status: crank_core::WorkspaceStatus::Active, + settings: json!({}), + created_at: "2026-03-25T12:00:00Z".to_owned(), + updated_at: "2026-03-25T12:00:00Z".to_owned(), + }; + let other_workspace = Workspace { + id: WorkspaceId::new("ws_profile_02"), + slug: "profile-other".to_owned(), + display_name: "Other Workspace".to_owned(), + status: crank_core::WorkspaceStatus::Active, + settings: json!({}), + created_at: "2026-03-25T12:01:00Z".to_owned(), + updated_at: "2026-03-25T12:01:00Z".to_owned(), + }; + let user_id = registry + .upsert_bootstrap_user("profile@example.com", "Owner", "hashed-password") + .await + .unwrap(); + + registry + .create_workspace(CreateWorkspaceRequest { + workspace: &workspace, + }) + .await + .unwrap(); + registry + .create_workspace(CreateWorkspaceRequest { + workspace: &other_workspace, + }) + .await + .unwrap(); + registry + .ensure_membership(&workspace.id, &user_id, MembershipRole::Owner) + .await + .unwrap(); + + let updated = registry + .update_user_profile(&user_id, "updated@example.com", "Updated Owner") + .await + .unwrap(); + let has_access = registry + .user_has_workspace_access(&user_id, &workspace.id) + .await + .unwrap(); + let lacks_access = registry + .user_has_workspace_access(&user_id, &other_workspace.id) + .await + .unwrap(); + + assert_eq!(updated.id, user_id); + assert_eq!(updated.email, "updated@example.com"); + assert_eq!(updated.display_name, "Updated Owner"); + assert!(has_access); + assert!(!lacks_access); + + database.cleanup().await; + } + #[tokio::test] async fn manages_platform_api_key_read_paths() { let database = TestDatabase::new().await;