update workspace sharing on all assets

This commit is contained in:
dal 2025-07-17 12:54:02 -06:00
parent 217dc9ca9d
commit a0a1e11493
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
55 changed files with 7435 additions and 118 deletions

View File

@ -927,6 +927,9 @@ pub async fn process_metric_file(
public_expiry_date: None,
version_history: VersionHistory::new(1, metric_yml.clone()),
data_metadata: metadata,
workspace_sharing: database::enums::WorkspaceSharing::None,
workspace_sharing_enabled_by: None,
workspace_sharing_enabled_at: None,
public_password: None,
data_source_id,
};
@ -1253,6 +1256,7 @@ pub fn apply_modifications_to_content(
mod tests {
use super::*;
use chrono::Utc;
use database::enums::WorkspaceSharing;
use database::models::DashboardFile;
use database::types::DashboardYml;
@ -1368,6 +1372,9 @@ rows:
public_expiry_date: None,
version_history,
public_password: None,
workspace_sharing: WorkspaceSharing::None,
workspace_sharing_enabled_at: None,
workspace_sharing_enabled_by: None,
};
// Create a file modification
@ -1543,6 +1550,9 @@ rows:
public_expiry_date: None,
version_history,
public_password: None,
workspace_sharing: WorkspaceSharing::None,
workspace_sharing_enabled_at: None,
workspace_sharing_enabled_by: None,
};
// Create a file modification that would match in multiple places

View File

@ -120,6 +120,9 @@ async fn process_dashboard_file(
public_expiry_date: None,
version_history: VersionHistory::new(1, dashboard_yml.clone()),
public_password: None,
workspace_sharing: database::enums::WorkspaceSharing::None,
workspace_sharing_enabled_by: None,
workspace_sharing_enabled_at: None,
};
Ok((dashboard_file, dashboard_yml))

View File

@ -726,3 +726,52 @@ impl FromSql<sql_types::MessageFeedbackEnum, Pg> for MessageFeedback {
}
}
}
// WorkspaceSharing enum
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
diesel::AsExpression,
diesel::FromSqlRow,
Deserialize,
Serialize,
)]
#[diesel(sql_type = sql_types::WorkspaceSharingEnum)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceSharing {
#[serde(alias = "none")]
None,
#[serde(alias = "canView")]
CanView,
#[serde(alias = "canEdit")]
CanEdit,
#[serde(alias = "fullAccess")]
FullAccess,
}
impl ToSql<sql_types::WorkspaceSharingEnum, Pg> for WorkspaceSharing {
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
match *self {
WorkspaceSharing::None => out.write_all(b"none")?,
WorkspaceSharing::CanView => out.write_all(b"can_view")?,
WorkspaceSharing::CanEdit => out.write_all(b"can_edit")?,
WorkspaceSharing::FullAccess => out.write_all(b"full_access")?,
}
Ok(IsNull::No)
}
}
impl FromSql<sql_types::WorkspaceSharingEnum, Pg> for WorkspaceSharing {
fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Self> {
match bytes.as_bytes() {
b"none" => Ok(WorkspaceSharing::None),
b"can_view" => Ok(WorkspaceSharing::CanView),
b"can_edit" => Ok(WorkspaceSharing::CanEdit),
b"full_access" => Ok(WorkspaceSharing::FullAccess),
_ => Err("Unrecognized WorkspaceSharing variant".into()),
}
}
}

View File

@ -1,4 +1,4 @@
use crate::enums::{AssetPermissionRole, AssetType, IdentityType, Verification};
use crate::enums::{AssetPermissionRole, AssetType, IdentityType, Verification, WorkspaceSharing};
use crate::models::{AssetPermission, DashboardFile, MetricFile, User};
use crate::pool::get_pg_pool;
use crate::types::metric_yml::{BarAndLineAxis, BarLineChartConfig, BaseChartConfig, ChartConfig};
@ -96,6 +96,9 @@ impl TestDb {
data_metadata: None,
public_password: None,
data_source_id: Uuid::new_v4(),
workspace_sharing: WorkspaceSharing::None,
workspace_sharing_enabled_by: None,
workspace_sharing_enabled_at: None,
};
Ok(metric_file)
@ -126,6 +129,9 @@ impl TestDb {
public_expiry_date: None,
version_history: VersionHistory(std::collections::HashMap::new()),
public_password: None,
workspace_sharing: WorkspaceSharing::None,
workspace_sharing_enabled_by: None,
workspace_sharing_enabled_at: None,
};
Ok(dashboard_file)

View File

@ -38,6 +38,9 @@ pub struct DashboardFile {
pub public_expiry_date: Option<DateTime<Utc>>,
pub version_history: VersionHistory,
pub public_password: Option<String>,
pub workspace_sharing: WorkspaceSharing,
pub workspace_sharing_enabled_by: Option<Uuid>,
pub workspace_sharing_enabled_at: Option<DateTime<Utc>>,
}
#[derive(Queryable, Insertable, Identifiable, Associations, Debug, Clone, Serialize)]
@ -97,6 +100,9 @@ pub struct MetricFile {
pub data_metadata: Option<DataMetadata>,
pub public_password: Option<String>,
pub data_source_id: Uuid,
pub workspace_sharing: WorkspaceSharing,
pub workspace_sharing_enabled_by: Option<Uuid>,
pub workspace_sharing_enabled_at: Option<DateTime<Utc>>,
}
#[derive(Queryable, Insertable, Identifiable, Associations, Debug, Clone, Serialize)]
@ -118,6 +124,9 @@ pub struct Chat {
pub most_recent_file_id: Option<Uuid>,
pub most_recent_file_type: Option<String>,
pub most_recent_version_number: Option<i32>,
pub workspace_sharing: WorkspaceSharing,
pub workspace_sharing_enabled_by: Option<Uuid>,
pub workspace_sharing_enabled_at: Option<DateTime<Utc>>,
}
#[derive(Queryable, Insertable, Associations, Debug)]
@ -331,6 +340,9 @@ pub struct Collection {
pub updated_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>,
pub organization_id: Uuid,
pub workspace_sharing: WorkspaceSharing,
pub workspace_sharing_enabled_by: Option<Uuid>,
pub workspace_sharing_enabled_at: Option<DateTime<Utc>>,
}
#[derive(Queryable, Insertable, Identifiable, Debug, Clone, Serialize, Deserialize)]

View File

@ -48,6 +48,10 @@ pub mod sql_types {
#[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "verification_enum"))]
pub struct VerificationEnum;
#[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "workspace_sharing_enum"))]
pub struct WorkspaceSharingEnum;
}
diesel::table! {
@ -83,6 +87,9 @@ diesel::table! {
}
diesel::table! {
use diesel::sql_types::*;
use super::sql_types::WorkspaceSharingEnum;
chats (id) {
id -> Uuid,
title -> Text,
@ -99,10 +106,16 @@ diesel::table! {
#[max_length = 255]
most_recent_file_type -> Nullable<Varchar>,
most_recent_version_number -> Nullable<Int4>,
workspace_sharing -> WorkspaceSharingEnum,
workspace_sharing_enabled_by -> Nullable<Uuid>,
workspace_sharing_enabled_at -> Nullable<Timestamptz>,
}
}
diesel::table! {
use diesel::sql_types::*;
use super::sql_types::WorkspaceSharingEnum;
collections (id) {
id -> Uuid,
name -> Text,
@ -113,6 +126,9 @@ diesel::table! {
updated_at -> Timestamptz,
deleted_at -> Nullable<Timestamptz>,
organization_id -> Uuid,
workspace_sharing -> WorkspaceSharingEnum,
workspace_sharing_enabled_by -> Nullable<Uuid>,
workspace_sharing_enabled_at -> Nullable<Timestamptz>,
}
}
@ -133,6 +149,9 @@ diesel::table! {
}
diesel::table! {
use diesel::sql_types::*;
use super::sql_types::WorkspaceSharingEnum;
dashboard_files (id) {
id -> Uuid,
name -> Varchar,
@ -149,6 +168,9 @@ diesel::table! {
public_expiry_date -> Nullable<Timestamptz>,
version_history -> Jsonb,
public_password -> Nullable<Text>,
workspace_sharing -> WorkspaceSharingEnum,
workspace_sharing_enabled_by -> Nullable<Uuid>,
workspace_sharing_enabled_at -> Nullable<Timestamptz>,
}
}
@ -392,6 +414,7 @@ diesel::table! {
diesel::table! {
use diesel::sql_types::*;
use super::sql_types::VerificationEnum;
use super::sql_types::WorkspaceSharingEnum;
metric_files (id) {
id -> Uuid,
@ -414,6 +437,9 @@ diesel::table! {
data_metadata -> Nullable<Jsonb>,
public_password -> Nullable<Text>,
data_source_id -> Uuid,
workspace_sharing -> WorkspaceSharingEnum,
workspace_sharing_enabled_by -> Nullable<Uuid>,
workspace_sharing_enabled_at -> Nullable<Timestamptz>,
}
}

View File

@ -55,6 +55,7 @@ pub async fn delete_chats_handler(
],
cwp.chat.organization_id,
&user.organizations,
cwp.chat.workspace_sharing,
);
has_permission

View File

@ -7,7 +7,7 @@ use uuid::Uuid;
use crate::chats::get_chat_handler::get_chat_handler;
use crate::chats::types::ChatWithMessages;
use database::enums::{AssetPermissionRole, AssetType, IdentityType};
use database::enums::{AssetPermissionRole, AssetType, IdentityType, WorkspaceSharing};
use database::helpers::chats::fetch_chat_with_permission;
use database::models::{AssetPermission, Chat, Message, MessageToFile};
use database::pool::get_pg_pool;
@ -49,6 +49,7 @@ pub async fn duplicate_chat_handler(
],
chat_with_permission.chat.organization_id,
&user.organizations,
chat_with_permission.chat.workspace_sharing,
);
// If user is the creator, they automatically have access
@ -83,6 +84,9 @@ pub async fn duplicate_chat_handler(
most_recent_file_id: source_chat.most_recent_file_id,
most_recent_file_type: source_chat.most_recent_file_type.clone(),
most_recent_version_number: source_chat.most_recent_version_number,
workspace_sharing: WorkspaceSharing::None,
workspace_sharing_enabled_at: None,
workspace_sharing_enabled_by: None,
};
// Insert the new chat record

View File

@ -93,6 +93,7 @@ pub async fn get_chat_handler(
&access_requirement,
chat_with_permission.chat.organization_id,
&user.organizations,
chat_with_permission.chat.workspace_sharing,
);
// If user is the creator, they automatically have access

View File

@ -1,7 +1,7 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
use database::{
enums::{AssetType, IdentityType},
enums::{AssetType, IdentityType, WorkspaceSharing},
pool::get_pg_pool,
};
use diesel::prelude::*;
@ -151,9 +151,68 @@ pub async fn list_chats_handler(
.load::<ChatWithUser>(&mut conn)
.await?;
// Get user's organization IDs
let user_org_ids: Vec<Uuid> = user.organizations.iter().map(|org| org.id).collect();
// Second query: Get workspace-shared chats that the user doesn't have direct access to
let workspace_shared_chats = if !request.admin_view && !user_org_ids.is_empty() {
chats::table
.inner_join(users::table.on(chats::created_by.eq(users::id)))
.filter(chats::deleted_at.is_null())
.filter(chats::title.ne(""))
.filter(chats::organization_id.eq_any(&user_org_ids))
.filter(chats::workspace_sharing.ne(WorkspaceSharing::None))
// Exclude chats we already have direct access to
.filter(
chats::created_by.ne(user.id).and(
diesel::dsl::not(diesel::dsl::exists(
asset_permissions::table
.filter(asset_permissions::asset_id.eq(chats::id))
.filter(asset_permissions::asset_type.eq(AssetType::Chat))
.filter(asset_permissions::identity_id.eq(user.id))
.filter(asset_permissions::identity_type.eq(IdentityType::User))
.filter(asset_permissions::deleted_at.is_null())
))
)
)
.filter(
diesel::dsl::exists(
messages::table
.filter(messages::chat_id.eq(chats::id))
.filter(messages::request_message.is_not_null())
.filter(messages::deleted_at.is_null())
).or(
diesel::dsl::sql::<diesel::sql_types::Bool>(
"(SELECT COUNT(*) FROM messages WHERE messages.chat_id = chats.id AND messages.deleted_at IS NULL) > 1"
)
)
)
.select((
chats::id,
chats::title,
chats::created_at,
chats::updated_at,
chats::created_by,
chats::most_recent_file_id,
chats::most_recent_file_type,
chats::most_recent_version_number,
users::name.nullable(),
users::avatar_url.nullable(),
))
.order_by(chats::updated_at.desc())
.offset(offset as i64)
.limit((request.page_size + 1) as i64)
.load::<ChatWithUser>(&mut conn)
.await?
} else {
vec![]
};
// Check if there are more results and prepare pagination info
let has_more = results.len() > request.page_size as usize;
let items: Vec<ChatListItem> = results
let has_more = results.len() > request.page_size as usize || workspace_shared_chats.len() > request.page_size as usize;
// Process directly-accessed chats
let mut items: Vec<ChatListItem> = results
.into_iter()
.filter(|chat| !chat.title.trim().is_empty()) // Filter out titles with only whitespace
.take(request.page_size as usize)
@ -177,6 +236,29 @@ pub async fn list_chats_handler(
})
.collect();
// Add workspace-shared chats (if we have room in the page)
let remaining_slots = request.page_size as usize - items.len();
for chat in workspace_shared_chats.into_iter().take(remaining_slots) {
if !chat.title.trim().is_empty() {
items.push(ChatListItem {
id: chat.id.to_string(),
name: chat.title,
is_favorited: false,
created_at: chat.created_at.to_rfc3339(),
updated_at: chat.updated_at.to_rfc3339(),
created_by: chat.created_by.to_string(),
created_by_id: chat.created_by.to_string(),
created_by_name: chat.user_name.unwrap_or_else(|| "Unknown".to_string()),
created_by_avatar: chat.user_avatar_url,
last_edited: chat.updated_at.to_rfc3339(),
latest_file_id: chat.most_recent_file_id.map(|id| id.to_string()),
latest_file_type: chat.most_recent_file_type,
latest_version_number: chat.most_recent_version_number,
is_shared: true, // Always true for workspace-shared chats
});
}
}
// Create pagination info
let _pagination = PaginationInfo {
has_more,

View File

@ -1,5 +1,6 @@
use agents::tools::file_tools::common::{generate_deterministic_uuid, ModifyFilesOutput};
use dashmap::DashMap;
use database::enums::WorkspaceSharing;
use middleware::AuthenticatedUser;
use std::collections::HashSet;
use std::env;
@ -2929,6 +2930,9 @@ async fn initialize_chat(
most_recent_file_id: None,
most_recent_file_type: None,
most_recent_version_number: None,
workspace_sharing: WorkspaceSharing::None,
workspace_sharing_enabled_at: None,
workspace_sharing_enabled_by: None,
};
// Create initial message using the *new* message ID

View File

@ -36,6 +36,7 @@ pub async fn create_chat_sharing_handler(
&[AssetPermissionRole::Owner, AssetPermissionRole::FullAccess],
chat.chat.organization_id,
&user.organizations,
chat.chat.workspace_sharing,
) {
return Err(anyhow!(
"Insufficient permissions to share this chat. You need Full Access or higher."

View File

@ -37,6 +37,7 @@ pub async fn delete_chat_sharing_handler(
&[AssetPermissionRole::Owner, AssetPermissionRole::FullAccess],
chat.chat.organization_id,
&user.organizations,
chat.chat.workspace_sharing,
) {
error!(
chat_id = %chat_id,

View File

@ -51,6 +51,7 @@ pub async fn list_chat_sharing_handler(
&[AssetPermissionRole::Owner, AssetPermissionRole::FullAccess, AssetPermissionRole::CanEdit, AssetPermissionRole::CanView],
chat.chat.organization_id,
&user.organizations,
chat.chat.workspace_sharing,
) {
error!(
chat_id = %chat_id,

View File

@ -67,6 +67,7 @@ pub async fn update_chat_sharing_handler(
&[AssetPermissionRole::Owner, AssetPermissionRole::FullAccess],
chat.chat.organization_id,
&user.organizations,
chat.chat.workspace_sharing,
) {
return Err(anyhow!(
"Insufficient permissions to update sharing for this chat. You need Full Access or higher."

View File

@ -65,6 +65,7 @@ pub async fn update_chats_handler(
&[AssetPermissionRole::CanEdit, AssetPermissionRole::FullAccess, AssetPermissionRole::Owner],
cwp.chat.organization_id,
&user.organizations,
cwp.chat.workspace_sharing,
);
is_creator || has_permission

View File

@ -106,35 +106,6 @@ mod tests {
assert_eq!(core_msg["content"], "Hello world");
}
#[test]
fn test_assistant_with_tool_call() {
let tool_call = ToolCall {
id: "call_123".to_string(),
function: litellm::Function {
name: "import_assets".to_string(),
arguments: r#"{"files": []}"#.to_string(),
},
};
let msg = AgentMessage::Assistant {
id: Some("assistant_123".to_string()),
content: None,
name: None,
tool_calls: Some(vec![tool_call]),
progress: MessageProgress::Complete,
initial: false,
};
let core_msg = agent_message_to_core_message(&msg).unwrap();
assert_eq!(core_msg["role"], "assistant");
assert!(core_msg["content"].is_array());
let content = core_msg["content"].as_array().unwrap();
assert_eq!(content.len(), 1);
assert_eq!(content[0]["type"], "tool-call");
assert_eq!(content[0]["toolName"], "import_assets");
}
#[test]
fn test_tool_result_conversion() {
let msg = AgentMessage::Tool {

View File

@ -93,6 +93,7 @@ pub async fn add_assets_to_collection_handler(
],
collection_with_permission.collection.organization_id,
&user.organizations,
collection_with_permission.collection.workspace_sharing,
);
if !has_permission {
@ -173,6 +174,7 @@ pub async fn add_assets_to_collection_handler(
],
dashboard.dashboard_file.organization_id,
&user.organizations,
dashboard.dashboard_file.workspace_sharing,
);
if !has_dashboard_permission {
@ -330,6 +332,7 @@ pub async fn add_assets_to_collection_handler(
],
metric.metric_file.organization_id,
&user.organizations,
metric.metric_file.workspace_sharing,
);
if !has_metric_permission {
@ -476,6 +479,7 @@ pub async fn add_assets_to_collection_handler(
],
chat_permission.chat.organization_id,
&user.organizations,
chat_permission.chat.workspace_sharing,
);
if !has_chat_permission {

View File

@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use chrono::Utc;
use database::{
enums::{AssetPermissionRole, AssetType, IdentityType},
enums::{AssetPermissionRole, AssetType, IdentityType, WorkspaceSharing},
models::{AssetPermission, Collection},
pool::get_pg_pool,
schema::{asset_permissions, collections},
@ -45,6 +45,9 @@ pub async fn create_collection_handler(
updated_by: user.id,
deleted_at: None,
organization_id,
workspace_sharing: WorkspaceSharing::None,
workspace_sharing_enabled_at: None,
workspace_sharing_enabled_by: None,
};
let insert_task_user_id = user.id;

View File

@ -49,6 +49,7 @@ pub async fn delete_collection_handler(
],
collection_with_permission.collection.organization_id,
&user.organizations,
collection_with_permission.collection.workspace_sharing,
);
if !has_permission {

View File

@ -91,6 +91,7 @@ pub async fn get_collection_handler(
],
collection_with_permission.collection.organization_id,
&user.organizations,
collection_with_permission.collection.workspace_sharing,
) {
return Err(anyhow!("You don't have permission to view this collection"));
}

View File

@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use database::{
enums::{AssetPermissionRole, AssetType, IdentityType},
enums::{AssetPermissionRole, AssetType, IdentityType, WorkspaceSharing},
pool::get_pg_pool,
schema::{asset_permissions, collections, teams_to_users, users},
};
@ -134,8 +134,68 @@ async fn get_permissioned_collections(
Err(e) => return Err(anyhow!("Error getting collection results: {}", e)),
};
// Get user's organization IDs
let user_org_ids: Vec<Uuid> = user.organizations.iter().map(|org| org.id).collect();
// Second query: Get workspace-shared collections that the user doesn't have direct access to
let workspace_shared_collections = collections::table
.inner_join(users::table.on(users::id.eq(collections::created_by)))
.filter(collections::deleted_at.is_null())
.filter(collections::organization_id.eq_any(&user_org_ids))
.filter(collections::workspace_sharing.ne(WorkspaceSharing::None))
// Exclude collections we already have direct access to
.filter(
diesel::dsl::not(diesel::dsl::exists(
asset_permissions::table
.filter(asset_permissions::asset_id.eq(collections::id))
.filter(asset_permissions::asset_type.eq(AssetType::Collection))
.filter(asset_permissions::deleted_at.is_null())
.filter(
asset_permissions::identity_id
.eq(user.id)
.or(teams_to_users::table
.filter(teams_to_users::team_id.eq(asset_permissions::identity_id))
.filter(teams_to_users::user_id.eq(user.id))
.filter(teams_to_users::deleted_at.is_null())
.select(teams_to_users::user_id)
.single_value()
.is_not_null()),
),
)),
)
.select((
collections::id,
collections::name,
collections::updated_at,
collections::created_at,
collections::workspace_sharing,
users::id,
users::name.nullable(),
users::email,
users::avatar_url.nullable(),
collections::organization_id,
))
.order((collections::updated_at.desc(), collections::id.asc()))
.offset(page * page_size)
.limit(page_size)
.load::<(
Uuid,
String,
DateTime<Utc>,
DateTime<Utc>,
WorkspaceSharing,
Uuid,
Option<String>,
String,
Option<String>,
Uuid,
)>(&mut conn)
.await
.map_err(|e| anyhow!("Error getting workspace-shared collections: {}", e))?;
let mut collections: Vec<ListCollectionsCollection> = Vec::new();
// Process directly-accessed collections
// Filter collections based on user permissions
// We'll include collections where the user has at least CanView permission
for (id, name, updated_at, created_at, role, creator_id, creator_name, email, creator_avatar_url, org_id) in collection_results {
@ -150,6 +210,7 @@ async fn get_permissioned_collections(
],
org_id,
&user.organizations,
WorkspaceSharing::None, // Use None as default for list handlers
);
if !has_permission {
@ -175,5 +236,52 @@ async fn get_permissioned_collections(
collections.push(collection);
}
// Process workspace-shared collections
for (id, name, updated_at, created_at, workspace_sharing, creator_id, creator_name, email, creator_avatar_url, org_id) in workspace_shared_collections {
// Map workspace sharing level to a permission role
let workspace_permission = match workspace_sharing {
WorkspaceSharing::CanView => AssetPermissionRole::CanView,
WorkspaceSharing::CanEdit => AssetPermissionRole::CanEdit,
WorkspaceSharing::FullAccess => AssetPermissionRole::FullAccess,
WorkspaceSharing::None => continue, // Skip if None
};
// Check if user has the workspace-granted permission
let has_permission = check_permission_access(
Some(workspace_permission),
&[
AssetPermissionRole::CanView,
AssetPermissionRole::CanEdit,
AssetPermissionRole::FullAccess,
AssetPermissionRole::Owner,
],
org_id,
&user.organizations,
workspace_sharing,
);
if !has_permission {
continue;
}
let owner = ListCollectionsUser {
id: creator_id,
name: creator_name.unwrap_or(email),
avatar_url: creator_avatar_url,
};
let collection = ListCollectionsCollection {
id,
name,
last_edited: updated_at,
created_at,
owner,
description: "".to_string(),
is_shared: true, // Always true for workspace-shared collections
};
collections.push(collection);
}
Ok(collections)
}

View File

@ -86,6 +86,7 @@ pub async fn remove_assets_from_collection_handler(
],
collection_with_permission.collection.organization_id,
&user.organizations,
collection_with_permission.collection.workspace_sharing,
);
if !has_permission {

View File

@ -59,6 +59,7 @@ pub async fn create_collection_sharing_handler(
],
collection_with_permission.collection.organization_id,
&user.organizations,
collection_with_permission.collection.workspace_sharing,
);
if !has_permission {

View File

@ -46,6 +46,7 @@ pub async fn delete_collection_sharing_handler(
&[AssetPermissionRole::FullAccess, AssetPermissionRole::Owner],
collection_with_permission.collection.organization_id,
&user.organizations,
collection_with_permission.collection.workspace_sharing,
);
if !has_permission {

View File

@ -52,6 +52,7 @@ pub async fn list_collection_sharing_handler(
],
collection_with_permission.collection.organization_id,
&user.organizations,
collection_with_permission.collection.workspace_sharing,
);
if !has_permission {

View File

@ -79,6 +79,7 @@ pub async fn update_collection_sharing_handler(
],
collection_with_permission.collection.organization_id,
&user.organizations,
collection_with_permission.collection.workspace_sharing,
);
if !has_permission {

View File

@ -50,6 +50,7 @@ pub async fn update_collection_handler(
],
collection_with_permission.collection.organization_id,
&user.organizations,
collection_with_permission.collection.workspace_sharing,
);
if !has_permission {

View File

@ -88,6 +88,7 @@ async fn delete_single_dashboard(dashboard_id: Uuid, user: &AuthenticatedUser) -
&[AssetPermissionRole::FullAccess, AssetPermissionRole::Owner],
dashboard_with_permission.dashboard_file.organization_id,
&user.organizations,
dashboard_with_permission.dashboard_file.workspace_sharing,
);
if !has_permission {

View File

@ -126,6 +126,7 @@ pub async fn get_dashboard_handler(
],
dashboard_file.organization_id,
&user.organizations,
dashboard_file.workspace_sharing,
);
tracing::debug!(dashboard_id = %dashboard_id, ?direct_permission_level, has_sufficient_direct_permission, "Direct permission check result");
@ -150,7 +151,7 @@ pub async fn get_dashboard_handler(
// No sufficient direct/admin permission, check if user has access via a chat
tracing::debug!(dashboard_id = %dashboard_id, "Insufficient direct/admin permission. Checking chat access.");
let has_chat_access = sharing::check_dashboard_chat_access(dashboard_id, &user.id)
let has_chat_access = sharing::check_dashboard_chat_access(dashboard_id, &user.id, &user.organizations)
.await
.unwrap_or(false);

View File

@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use database::{
enums::{AssetPermissionRole, AssetType, IdentityType, Verification},
enums::{AssetPermissionRole, AssetType, IdentityType, Verification, WorkspaceSharing},
pool::get_pg_pool,
schema::{asset_permissions, dashboard_files, teams_to_users, users},
};
@ -41,7 +41,8 @@ pub async fn list_dashboard_handler(
let offset = request.page_token * request.page_size;
// Build the query to get dashboards with permissions
// This is similar to how collections are queried
// We need to handle both direct permissions and workspace sharing
// First, get dashboards with direct permissions
let mut dashboard_statement = dashboard_files::table
.inner_join(
asset_permissions::table.on(dashboard_files::id
@ -66,6 +67,7 @@ pub async fn list_dashboard_handler(
users::name.nullable(),
users::avatar_url.nullable(),
dashboard_files::organization_id,
dashboard_files::workspace_sharing,
))
.filter(dashboard_files::deleted_at.is_null())
.filter(
@ -109,6 +111,7 @@ pub async fn list_dashboard_handler(
Option<String>,
Option<String>,
Uuid,
WorkspaceSharing,
)>(&mut conn)
.await
{
@ -120,7 +123,7 @@ pub async fn list_dashboard_handler(
// We'll include dashboards where the user has at least CanView permission
let mut dashboards = Vec::new();
for (id, name, created_by, created_at, updated_at, role, creator_name, creator_avatar_url, org_id) in
for (id, name, created_by, created_at, updated_at, role, creator_name, creator_avatar_url, org_id, workspace_sharing) in
dashboard_results
{
// Check if user has at least CanView permission
@ -134,6 +137,7 @@ pub async fn list_dashboard_handler(
],
org_id,
&user.organizations,
workspace_sharing,
);
if !has_permission {
@ -160,5 +164,106 @@ pub async fn list_dashboard_handler(
dashboards.push(dashboard_item);
}
Ok(dashboards)
// Now also fetch workspace-shared dashboards that the user doesn't have direct access to
let user_org_ids: Vec<Uuid> = user.organizations.iter().map(|org| org.id).collect();
if !user_org_ids.is_empty() {
let workspace_shared_dashboards = dashboard_files::table
.inner_join(users::table.on(users::id.eq(dashboard_files::created_by)))
.filter(dashboard_files::deleted_at.is_null())
.filter(dashboard_files::organization_id.eq_any(&user_org_ids))
.filter(dashboard_files::workspace_sharing.ne(WorkspaceSharing::None))
// Exclude dashboards we already have direct access to
.filter(
diesel::dsl::not(diesel::dsl::exists(
asset_permissions::table
.filter(asset_permissions::asset_id.eq(dashboard_files::id))
.filter(asset_permissions::asset_type.eq(AssetType::DashboardFile))
.filter(asset_permissions::deleted_at.is_null())
.filter(
asset_permissions::identity_id
.eq(user.id)
.or(
asset_permissions::identity_type.eq(IdentityType::Team)
.and(diesel::dsl::exists(
teams_to_users::table
.filter(teams_to_users::team_id.eq(asset_permissions::identity_id))
.filter(teams_to_users::user_id.eq(user.id))
.filter(teams_to_users::deleted_at.is_null())
))
)
)
))
)
.select((
dashboard_files::id,
dashboard_files::name,
dashboard_files::created_by,
dashboard_files::created_at,
dashboard_files::updated_at,
dashboard_files::workspace_sharing,
users::name.nullable(),
users::avatar_url.nullable(),
dashboard_files::organization_id,
))
.order((
dashboard_files::updated_at.desc(),
dashboard_files::id.asc(),
))
.load::<(
Uuid,
String,
Uuid,
DateTime<Utc>,
DateTime<Utc>,
WorkspaceSharing,
Option<String>,
Option<String>,
Uuid,
)>(&mut conn)
.await
.map_err(|e| anyhow!("Error getting workspace shared dashboards: {}", e))?;
for (id, name, created_by, created_at, updated_at, workspace_sharing, creator_name, creator_avatar_url, org_id) in workspace_shared_dashboards {
// Determine the effective permission based on workspace sharing
let role = match workspace_sharing {
WorkspaceSharing::CanView => AssetPermissionRole::CanView,
WorkspaceSharing::CanEdit => AssetPermissionRole::CanEdit,
WorkspaceSharing::FullAccess => AssetPermissionRole::FullAccess,
WorkspaceSharing::None => continue, // Should not happen due to filter
};
let owner = DashboardMember {
id: created_by,
name: creator_name.unwrap_or_else(|| "Unknown".to_string()),
avatar_url: creator_avatar_url,
};
let dashboard_item = BusterDashboardListItem {
id,
name,
created_at,
last_edited: updated_at,
owner,
members: vec![],
status: Verification::Verified,
is_shared: true, // Always shared for workspace-shared dashboards
};
dashboards.push(dashboard_item);
}
}
// Sort dashboards by updated_at desc
dashboards.sort_by(|a, b| b.last_edited.cmp(&a.last_edited));
// Apply pagination after combining results
let start = request.page_token as usize * request.page_size as usize;
let end = start + request.page_size as usize;
let paginated_dashboards = dashboards.into_iter()
.skip(start)
.take(request.page_size as usize)
.collect();
Ok(paginated_dashboards)
}

View File

@ -53,6 +53,7 @@ pub async fn create_dashboard_sharing_handler(
],
dashboard_with_permission.dashboard_file.organization_id,
&user.organizations,
dashboard_with_permission.dashboard_file.workspace_sharing,
);
if !has_permission {

View File

@ -53,6 +53,7 @@ pub async fn delete_dashboard_sharing_handler(
],
dashboard_with_permission.dashboard_file.organization_id,
&user.organizations,
dashboard_with_permission.dashboard_file.workspace_sharing,
);
if !has_permission {

View File

@ -53,6 +53,7 @@ pub async fn list_dashboard_sharing_handler(
],
dashboard_with_permission.dashboard_file.organization_id,
&user.organizations,
dashboard_with_permission.dashboard_file.workspace_sharing,
);
if !has_permission {

View File

@ -81,6 +81,7 @@ pub async fn update_dashboard_sharing_handler(
],
dashboard_with_permission.dashboard_file.organization_id,
&user.organizations,
dashboard_with_permission.dashboard_file.workspace_sharing,
);
if !has_permission {

View File

@ -86,6 +86,7 @@ pub async fn update_dashboard_handler(
],
dashboard_with_permission.dashboard_file.organization_id,
&user.organizations,
dashboard_with_permission.dashboard_file.workspace_sharing,
);
if !has_permission {

View File

@ -56,6 +56,7 @@ pub async fn delete_message_handler(user: AuthenticatedUser, message_id: Uuid) -
],
chat_with_permission.chat.organization_id,
&user.organizations,
chat_with_permission.chat.workspace_sharing,
);
// If user is the creator, they automatically have access

View File

@ -3,7 +3,7 @@ use database::helpers::metric_files::fetch_metric_files_with_permissions;
use futures::future::join_all;
use middleware::AuthenticatedUser;
use sharing::check_permission_access;
use database::enums::AssetPermissionRole;
use database::enums::{AssetPermissionRole, WorkspaceSharing};
use std::collections::HashMap;
use uuid::Uuid;
@ -143,6 +143,7 @@ pub async fn bulk_update_metrics_handler(
],
*organization_id,
&user.organizations,
WorkspaceSharing::None, // Use None as default for bulk handlers
) {
// User has permission, process the update
futures.push(process_single_update(update, user));

View File

@ -57,6 +57,7 @@ pub async fn delete_metric_handler(metric_id: &Uuid, user: &AuthenticatedUser) -
&[AssetPermissionRole::FullAccess, AssetPermissionRole::Owner],
metric_file.metric_file.organization_id,
&user.organizations,
metric_file.metric_file.workspace_sharing,
) {
return Err(anyhow!("You don't have permission to delete this metric"));
}
@ -115,6 +116,7 @@ pub async fn delete_metrics_handler(
&[AssetPermissionRole::FullAccess, AssetPermissionRole::Owner],
metric_with_permission.metric_file.organization_id,
&user.organizations,
metric_with_permission.metric_file.workspace_sharing,
) {
// Set the deleted_at timestamp for this metric
match diesel::update(metric_files::table)

View File

@ -9,6 +9,7 @@ use diesel_async::RunQueryDsl;
use indexmap::IndexMap;
use middleware::AuthenticatedUser;
use serde::{Deserialize, Serialize};
use sharing::asset_access_checks::check_metric_collection_access;
use uuid::Uuid;
use query_engine::data_types::DataType;
@ -72,7 +73,7 @@ pub async fn get_metric_data_handler(
);
// Check if user has access to ANY dashboard containing this metric (including public dashboards)
let has_dashboard_access = sharing::check_metric_dashboard_access(&request.metric_id, &user.id)
let has_dashboard_access = sharing::check_metric_dashboard_access(&request.metric_id, &user.id, &user.organizations)
.await
.unwrap_or(false);
@ -102,7 +103,7 @@ pub async fn get_metric_data_handler(
} else {
// No dashboard access, check if user has access via a chat
tracing::info!("No dashboard association found. Checking chat access.");
let has_chat_access = sharing::check_metric_chat_access(&request.metric_id, &user.id)
let has_chat_access = sharing::check_metric_chat_access(&request.metric_id, &user.id, &user.organizations)
.await
.unwrap_or(false);
@ -130,9 +131,40 @@ pub async fn get_metric_data_handler(
}
}
} else {
// No dashboard or chat access, return the original permission error
tracing::warn!("No dashboard or chat association found for metric. Returning original error.");
return Err(e);
// No chat access, check if user has access via a collection
tracing::info!("No chat association found. Checking collection access.");
let has_collection_access = check_metric_collection_access(&request.metric_id, &user.id, &user.organizations)
.await
.unwrap_or(false);
if has_collection_access {
// User has access to a collection containing this metric
tracing::info!("Found associated collection with user access. Fetching metric with collection context.");
match get_metric_for_dashboard_handler(
&request.metric_id,
&user,
request.version_number,
request.password.clone(),
)
.await
{
Ok(metric_via_collection) => {
tracing::debug!(
"Successfully retrieved metric via collection association."
);
metric_via_collection // Use this metric definition
}
Err(fetch_err) => {
// If fetching via collection fails unexpectedly, return that error
tracing::error!("Failed to fetch metric via collection context: {}", fetch_err);
return Err(fetch_err);
}
}
} else {
// No dashboard, chat, or collection access, return the original permission error
tracing::warn!("No dashboard, chat, or collection association found for metric. Returning original error.");
return Err(e);
}
}
}
} else {

View File

@ -6,6 +6,7 @@ use diesel_async::{AsyncPgConnection, RunQueryDsl};
use futures::future::join;
use middleware::AuthenticatedUser;
use serde_yaml;
use sharing::asset_access_checks::check_metric_collection_access;
use uuid::Uuid;
use crate::metrics::types::{AssociatedCollection, AssociatedDashboard, BusterMetric, BusterShareIndividual, Dataset};
@ -136,6 +137,7 @@ pub async fn get_metric_for_dashboard_handler(
],
metric_file.organization_id,
&user.organizations,
metric_file.workspace_sharing,
);
tracing::debug!(metric_id = %metric_id, ?direct_permission_level, has_sufficient_direct_permission, "Direct permission check result");
@ -160,7 +162,7 @@ pub async fn get_metric_for_dashboard_handler(
// No sufficient direct/admin permission, check if user has access via a dashboard
tracing::debug!(metric_id = %metric_id, "Insufficient direct/admin permission. Checking dashboard access.");
let has_dashboard_access = sharing::check_metric_dashboard_access(metric_id, &user.id)
let has_dashboard_access = sharing::check_metric_dashboard_access(metric_id, &user.id, &user.organizations)
.await
.unwrap_or(false);
@ -172,7 +174,7 @@ pub async fn get_metric_for_dashboard_handler(
// No dashboard access, check if user has access via a chat
tracing::debug!(metric_id = %metric_id, "No dashboard access. Checking chat access.");
let has_chat_access = sharing::check_metric_chat_access(metric_id, &user.id)
let has_chat_access = sharing::check_metric_chat_access(metric_id, &user.id, &user.organizations)
.await
.unwrap_or(false);
@ -181,16 +183,28 @@ pub async fn get_metric_for_dashboard_handler(
tracing::debug!(metric_id = %metric_id, user_id = %user.id, "User has access via chat. Granting CanView.");
permission = AssetPermissionRole::CanView;
} else {
// No chat access either, check public access rules
tracing::debug!(metric_id = %metric_id, "No chat access. Checking public access rules.");
if !metric_file.publicly_accessible {
tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Permission denied (not public, no dashboard/chat access, insufficient direct permission).");
return Err(anyhow!("You don't have permission to view this metric"));
}
tracing::debug!(metric_id = %metric_id, "Metric is publicly accessible.");
// No chat access, check if user has access via a collection
tracing::debug!(metric_id = %metric_id, "No chat access. Checking collection access.");
let has_collection_access = check_metric_collection_access(metric_id, &user.id, &user.organizations)
.await
.unwrap_or(false);
if has_collection_access {
// User has access to a collection containing this metric, grant CanView
tracing::debug!(metric_id = %metric_id, user_id = %user.id, "User has access via collection. Granting CanView.");
permission = AssetPermissionRole::CanView;
} else {
// No collection access either, check public access rules
tracing::debug!(metric_id = %metric_id, "No collection access. Checking public access rules.");
if !metric_file.publicly_accessible {
tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Permission denied (not public, no dashboard/chat/collection access, insufficient direct permission).");
return Err(anyhow!("You don't have permission to view this metric"));
}
tracing::debug!(metric_id = %metric_id, "Metric is publicly accessible.");
// Check if the public access has expired
if let Some(expiry_date) = metric_file.public_expiry_date {
// Check if the public access has expired
if let Some(expiry_date) = metric_file.public_expiry_date {
tracing::debug!(metric_id = %metric_id, ?expiry_date, "Checking expiry date");
if expiry_date < chrono::Utc::now() {
tracing::warn!(metric_id = %metric_id, "Public access expired");
@ -219,10 +233,11 @@ pub async fn get_metric_for_dashboard_handler(
return Err(anyhow!("public_password required for this metric"));
}
}
} else {
// Publicly accessible, not expired, and no password required
tracing::debug!(metric_id = %metric_id, "Public access granted (no password required).");
permission = AssetPermissionRole::CanView;
} else {
// Publicly accessible, not expired, and no password required
tracing::debug!(metric_id = %metric_id, "Public access granted (no password required).");
permission = AssetPermissionRole::CanView;
}
}
}
}

View File

@ -4,6 +4,7 @@ use diesel_async::RunQueryDsl;
use futures::future::join;
use middleware::AuthenticatedUser;
use serde_yaml;
use sharing::asset_access_checks::check_metric_collection_access;
use uuid::Uuid;
use crate::metrics::types::{AssociatedCollection, AssociatedDashboard, BusterMetric, Dataset};
@ -134,6 +135,7 @@ pub async fn get_metric_handler(
],
metric_file.organization_id,
&user.organizations,
metric_file.workspace_sharing,
);
tracing::debug!(metric_id = %metric_id, ?direct_permission_level, has_sufficient_direct_permission, "Direct permission check result");
@ -158,7 +160,7 @@ pub async fn get_metric_handler(
// No sufficient direct/admin permission, check if user has access via a dashboard
tracing::debug!(metric_id = %metric_id, "Insufficient direct/admin permission. Checking dashboard access.");
let has_dashboard_access = sharing::check_metric_dashboard_access(metric_id, &user.id)
let has_dashboard_access = sharing::check_metric_dashboard_access(metric_id, &user.id, &user.organizations)
.await
.unwrap_or(false);
@ -170,7 +172,7 @@ pub async fn get_metric_handler(
// No dashboard access, check if user has access via a chat
tracing::debug!(metric_id = %metric_id, "No dashboard access. Checking chat access.");
let has_chat_access = sharing::check_metric_chat_access(metric_id, &user.id)
let has_chat_access = sharing::check_metric_chat_access(metric_id, &user.id, &user.organizations)
.await
.unwrap_or(false);
@ -179,16 +181,28 @@ pub async fn get_metric_handler(
tracing::debug!(metric_id = %metric_id, user_id = %user.id, "User has access via chat. Granting CanView.");
permission = AssetPermissionRole::CanView;
} else {
// No chat access either, check public access rules
tracing::debug!(metric_id = %metric_id, "No chat access. Checking public access rules.");
if !metric_file.publicly_accessible {
tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Permission denied (not public, no dashboard/chat access, insufficient direct permission).");
return Err(anyhow!("You don't have permission to view this metric"));
}
tracing::debug!(metric_id = %metric_id, "Metric is publicly accessible.");
// No chat access, check if user has access via a collection
tracing::debug!(metric_id = %metric_id, "No chat access. Checking collection access.");
let has_collection_access = check_metric_collection_access(metric_id, &user.id, &user.organizations)
.await
.unwrap_or(false);
if has_collection_access {
// User has access to a collection containing this metric, grant CanView
tracing::debug!(metric_id = %metric_id, user_id = %user.id, "User has access via collection. Granting CanView.");
permission = AssetPermissionRole::CanView;
} else {
// No collection access either, check public access rules
tracing::debug!(metric_id = %metric_id, "No collection access. Checking public access rules.");
if !metric_file.publicly_accessible {
tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Permission denied (not public, no dashboard/chat/collection access, insufficient direct permission).");
return Err(anyhow!("You don't have permission to view this metric"));
}
tracing::debug!(metric_id = %metric_id, "Metric is publicly accessible.");
// Check if the public access has expired
if let Some(expiry_date) = metric_file.public_expiry_date {
// Check if the public access has expired
if let Some(expiry_date) = metric_file.public_expiry_date {
tracing::debug!(metric_id = %metric_id, ?expiry_date, "Checking expiry date");
if expiry_date < chrono::Utc::now() {
tracing::warn!(metric_id = %metric_id, "Public access expired");
@ -217,10 +231,11 @@ pub async fn get_metric_handler(
return Err(anyhow!("public_password required for this metric"));
}
}
} else {
// Publicly accessible, not expired, and no password required
tracing::debug!(metric_id = %metric_id, "Public access granted (no password required).");
permission = AssetPermissionRole::CanView;
} else {
// Publicly accessible, not expired, and no password required
tracing::debug!(metric_id = %metric_id, "Public access granted (no password required).");
permission = AssetPermissionRole::CanView;
}
}
}
}

View File

@ -1,9 +1,9 @@
use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use database::{
enums::{AssetPermissionRole, AssetType, IdentityType, Verification},
enums::{AssetPermissionRole, AssetType, IdentityType, Verification, WorkspaceSharing},
pool::get_pg_pool,
schema::{asset_permissions, metric_files, users},
schema::{asset_permissions, metric_files, users, teams_to_users},
};
use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
@ -80,7 +80,7 @@ pub async fn list_metrics_handler(
.into_boxed();
// Add filters based on request parameters
if let Some(verification_statuses) = request.verification {
if let Some(verification_statuses) = request.verification.clone() {
// Only apply filter if the vec is not empty
if !verification_statuses.is_empty() {
metric_statement = metric_statement.filter(metric_files::verification.eq_any(verification_statuses));
@ -126,8 +126,82 @@ pub async fn list_metrics_handler(
Err(e) => return Err(anyhow!("Error getting metric results: {}", e)),
};
// Get user's organization IDs
let user_org_ids: Vec<Uuid> = user.organizations.iter().map(|org| org.id).collect();
// Second query: Get workspace-shared metrics that the user doesn't have direct access to
let mut workspace_shared_statement = metric_files::table
.inner_join(users::table.on(metric_files::created_by.eq(users::id)))
.filter(metric_files::deleted_at.is_null())
.filter(metric_files::organization_id.eq_any(&user_org_ids))
.filter(metric_files::workspace_sharing.ne(WorkspaceSharing::None))
// Exclude metrics we already have direct access to
.filter(
diesel::dsl::not(diesel::dsl::exists(
asset_permissions::table
.filter(asset_permissions::asset_id.eq(metric_files::id))
.filter(asset_permissions::asset_type.eq(AssetType::MetricFile))
.filter(asset_permissions::deleted_at.is_null())
.filter(
asset_permissions::identity_id
.eq(user.id)
.or(teams_to_users::table
.filter(teams_to_users::team_id.eq(asset_permissions::identity_id))
.filter(teams_to_users::user_id.eq(user.id))
.filter(teams_to_users::deleted_at.is_null())
.select(teams_to_users::user_id)
.single_value()
.is_not_null()),
),
)),
)
.select((
metric_files::id,
metric_files::name,
metric_files::created_by,
metric_files::created_at,
metric_files::updated_at,
metric_files::verification,
metric_files::workspace_sharing,
users::name.nullable(),
users::email,
users::avatar_url.nullable(),
))
.order((metric_files::updated_at.desc(), metric_files::id.asc()))
.offset(offset)
.limit(request.page_size)
.into_boxed();
// Apply filters to workspace-shared query
if let Some(verification_statuses) = &request.verification {
if !verification_statuses.is_empty() {
workspace_shared_statement = workspace_shared_statement.filter(metric_files::verification.eq_any(verification_statuses));
}
}
// Don't include workspace-shared metrics for only_my_metrics filter
let workspace_shared_results = if request.only_my_metrics == Some(true) {
vec![]
} else {
workspace_shared_statement
.load::<(
Uuid,
String,
Uuid,
DateTime<Utc>,
DateTime<Utc>,
Verification,
WorkspaceSharing,
Option<String>,
String,
Option<String>,
)>(&mut conn)
.await
.map_err(|e| anyhow!("Error getting workspace-shared metrics: {}", e))?
};
// Transform query results into BusterMetricListItem
let metrics = metric_results
let mut metrics: Vec<BusterMetricListItem> = metric_results
.into_iter()
.map(
|(
@ -149,5 +223,20 @@ pub async fn list_metrics_handler(
)
.collect();
// Add workspace-shared metrics
for (id, name, created_by, _created_at, updated_at, status, _workspace_sharing, created_by_name, created_by_email, created_by_avatar) in workspace_shared_results {
metrics.push(BusterMetricListItem {
id,
name,
last_edited: updated_at,
created_by_id: created_by,
created_by_name: created_by_name.unwrap_or(created_by_email.clone()),
created_by_email,
created_by_avatar,
status,
is_shared: true, // Always true for workspace-shared metrics
});
}
Ok(metrics)
}

View File

@ -47,6 +47,7 @@ pub async fn create_metric_sharing_handler(
&[AssetPermissionRole::FullAccess, AssetPermissionRole::Owner],
metric_file.metric_file.organization_id,
&user.organizations,
metric_file.metric_file.workspace_sharing,
) {
return Err(anyhow!("You don't have permission to share this metric"));
}

View File

@ -47,6 +47,7 @@ pub async fn delete_metric_sharing_handler(
&[AssetPermissionRole::FullAccess, AssetPermissionRole::Owner],
metric_file.metric_file.organization_id,
&user.organizations,
metric_file.metric_file.workspace_sharing,
) {
return Err(anyhow!(
"You don't have permission to delete sharing for this metric"

View File

@ -78,6 +78,7 @@ pub async fn update_metric_sharing_handler(
&[AssetPermissionRole::FullAccess, AssetPermissionRole::Owner],
metric_file.metric_file.organization_id,
&user.organizations,
metric_file.metric_file.workspace_sharing,
) {
return Err(anyhow!(
"You don't have permission to update sharing for this metric"

View File

@ -1,7 +1,7 @@
use anyhow::{anyhow, bail, Result};
use chrono::{DateTime, Utc};
use database::{
enums::{AssetPermissionRole, AssetType, DataSourceType, IdentityType, Verification},
enums::{AssetPermissionRole, AssetType, DataSourceType, IdentityType, Verification, WorkspaceSharing},
helpers::metric_files::fetch_metric_file_with_permissions,
models::{Dataset, MetricFile, MetricFileToDataset},
pool::get_pg_pool,
@ -131,6 +131,7 @@ pub async fn update_metric_handler(
],
organization_id,
&user.organizations,
WorkspaceSharing::None, // Use None as default for update handlers
) {
return Err(anyhow!(
"You don't have permission to update this metric. Editor or higher role required."

View File

@ -1,6 +1,6 @@
use database::enums::{AssetPermissionRole, AssetType, IdentityType, UserOrganizationRole};
use database::enums::{AssetPermissionRole, AssetType, IdentityType, UserOrganizationRole, WorkspaceSharing};
use database::pool::get_pg_pool;
use database::schema::{asset_permissions, dashboard_files, metric_files_to_dashboard_files};
use database::schema::{asset_permissions, dashboard_files, metric_files_to_dashboard_files, collections, collections_to_assets};
use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl, OptionalExtension};
use diesel_async::RunQueryDsl;
use middleware::OrganizationMembership;
@ -13,6 +13,7 @@ use uuid::Uuid;
/// * `required_permission_level` - Required permission level to access the asset
/// * `organization_id` - UUID of the organization
/// * `organization_role_grants` - Array of tuples containing (UUID, UserOrganizationRole) for the user
/// * `workspace_sharing` - Workspace sharing level for the asset
///
/// # Returns
/// * `bool` - True if the user has sufficient permissions, false otherwise
@ -21,6 +22,7 @@ pub fn check_permission_access(
required_permission_level: &[AssetPermissionRole],
organization_id: Uuid,
organization_role_grants: &[OrganizationMembership],
workspace_sharing: WorkspaceSharing,
) -> bool {
// First check if the user has WorkspaceAdmin or DataAdmin role for the organization
for org in organization_role_grants {
@ -32,6 +34,25 @@ pub fn check_permission_access(
}
}
// Check if user is member of the workspace and asset is shared
if workspace_sharing != WorkspaceSharing::None {
for org in organization_role_grants {
if org.id == organization_id {
// Map workspace sharing level to permission role
let workspace_permission = match workspace_sharing {
WorkspaceSharing::CanView => AssetPermissionRole::CanView,
WorkspaceSharing::CanEdit => AssetPermissionRole::CanEdit,
WorkspaceSharing::FullAccess => AssetPermissionRole::FullAccess,
WorkspaceSharing::None => unreachable!(),
};
if required_permission_level.contains(&workspace_permission) {
return true;
}
}
}
}
// Then check if the user has the required permission level
if let Some(permission) = current_permission_level {
if required_permission_level.contains(&permission) {
@ -46,18 +67,20 @@ pub fn check_permission_access(
/// Checks if a user has access to a metric through any associated dashboard.
///
/// This function is used to implement permission cascading from dashboards to metrics.
/// If a user has access to any dashboard containing the metric (either through direct permissions
/// or if the dashboard is public), they get at least CanView permission.
/// If a user has access to any dashboard containing the metric (either through direct permissions,
/// workspace sharing, or if the dashboard is public), they get at least CanView permission.
///
/// # Arguments
/// * `metric_id` - UUID of the metric to check
/// * `user_id` - UUID of the user to check permissions for
/// * `user_orgs` - User's organization memberships
///
/// # Returns
/// * `Result<bool>` - True if the user has access to any dashboard containing the metric, false otherwise
pub async fn check_metric_dashboard_access(
metric_id: &Uuid,
user_id: &Uuid,
user_orgs: &[OrganizationMembership],
) -> Result<bool, diesel::result::Error> {
let mut conn = get_pg_pool().get().await.map_err(|e| {
diesel::result::Error::DatabaseError(
@ -114,23 +137,52 @@ pub async fn check_metric_dashboard_access(
.await
.optional()?;
Ok(has_public_access.is_some())
if has_public_access.is_some() {
return Ok(true);
}
// Check if metric belongs to any workspace-shared dashboard
let workspace_shared_dashboard = metric_files_to_dashboard_files::table
.inner_join(
dashboard_files::table
.on(dashboard_files::id.eq(metric_files_to_dashboard_files::dashboard_file_id)),
)
.filter(metric_files_to_dashboard_files::metric_file_id.eq(metric_id))
.filter(dashboard_files::deleted_at.is_null())
.filter(metric_files_to_dashboard_files::deleted_at.is_null())
.filter(dashboard_files::workspace_sharing.ne(WorkspaceSharing::None))
.select((dashboard_files::organization_id, dashboard_files::workspace_sharing))
.first::<(Uuid, WorkspaceSharing)>(&mut conn)
.await
.optional()?;
if let Some((org_id, _sharing_level)) = workspace_shared_dashboard {
// Check if user is member of that organization
if user_orgs.iter().any(|org| org.id == org_id) {
return Ok(true);
}
}
Ok(false)
}
/// Checks if a user has access to a metric through any associated chat.
///
/// This function is used to implement permission cascading from chats to metrics.
/// If a user has access to any chat containing the metric, they get at least CanView permission.
/// If a user has access to any chat containing the metric (either through direct permissions,
/// workspace sharing, or if the chat is public), they get at least CanView permission.
///
/// # Arguments
/// * `metric_id` - UUID of the metric to check
/// * `user_id` - UUID of the user to check permissions for
/// * `user_orgs` - User's organization memberships
///
/// # Returns
/// * `Result<bool>` - True if the user has access to any chat containing the metric, false otherwise
pub async fn check_metric_chat_access(
metric_id: &Uuid,
user_id: &Uuid,
user_orgs: &[OrganizationMembership],
) -> Result<bool, diesel::result::Error> {
let mut conn = get_pg_pool().get().await.map_err(|e| {
diesel::result::Error::DatabaseError(
@ -197,23 +249,57 @@ pub async fn check_metric_chat_access(
.await
.optional()?;
Ok(has_public_chat_access.is_some())
if has_public_chat_access.is_some() {
return Ok(true);
}
// Check if metric belongs to any workspace-shared chat
let workspace_shared_chat = database::schema::messages_to_files::table
.inner_join(
database::schema::messages::table
.on(database::schema::messages::id.eq(database::schema::messages_to_files::message_id)),
)
.inner_join(
database::schema::chats::table
.on(database::schema::chats::id.eq(database::schema::messages::chat_id)),
)
.filter(database::schema::messages_to_files::file_id.eq(metric_id))
.filter(database::schema::messages_to_files::deleted_at.is_null())
.filter(database::schema::messages::deleted_at.is_null())
.filter(database::schema::chats::deleted_at.is_null())
.filter(database::schema::chats::workspace_sharing.ne(WorkspaceSharing::None))
.select((database::schema::chats::organization_id, database::schema::chats::workspace_sharing))
.first::<(Uuid, WorkspaceSharing)>(&mut conn)
.await
.optional()?;
if let Some((org_id, _sharing_level)) = workspace_shared_chat {
// Check if user is member of that organization
if user_orgs.iter().any(|org| org.id == org_id) {
return Ok(true);
}
}
Ok(false)
}
/// Checks if a user has access to a dashboard through any associated chat.
///
/// This function is used to implement permission cascading from chats to dashboards.
/// If a user has access to any chat containing the dashboard, they get at least CanView permission.
/// If a user has access to any chat containing the dashboard (either through direct permissions,
/// workspace sharing, or if the chat is public), they get at least CanView permission.
///
/// # Arguments
/// * `dashboard_id` - UUID of the dashboard to check
/// * `user_id` - UUID of the user to check permissions for
/// * `user_orgs` - User's organization memberships
///
/// # Returns
/// * `Result<bool>` - True if the user has access to any chat containing the dashboard, false otherwise
pub async fn check_dashboard_chat_access(
dashboard_id: &Uuid,
user_id: &Uuid,
user_orgs: &[OrganizationMembership],
) -> Result<bool, diesel::result::Error> {
let mut conn = get_pg_pool().get().await.map_err(|e| {
diesel::result::Error::DatabaseError(
@ -280,7 +366,200 @@ pub async fn check_dashboard_chat_access(
.await
.optional()?;
Ok(has_public_chat_access.is_some())
if has_public_chat_access.is_some() {
return Ok(true);
}
// Check if dashboard belongs to any workspace-shared chat
let workspace_shared_chat = database::schema::messages_to_files::table
.inner_join(
database::schema::messages::table
.on(database::schema::messages::id.eq(database::schema::messages_to_files::message_id)),
)
.inner_join(
database::schema::chats::table
.on(database::schema::chats::id.eq(database::schema::messages::chat_id)),
)
.filter(database::schema::messages_to_files::file_id.eq(dashboard_id))
.filter(database::schema::messages_to_files::deleted_at.is_null())
.filter(database::schema::messages::deleted_at.is_null())
.filter(database::schema::chats::deleted_at.is_null())
.filter(database::schema::chats::workspace_sharing.ne(WorkspaceSharing::None))
.select((database::schema::chats::organization_id, database::schema::chats::workspace_sharing))
.first::<(Uuid, WorkspaceSharing)>(&mut conn)
.await
.optional()?;
if let Some((org_id, _sharing_level)) = workspace_shared_chat {
// Check if user is member of that organization
if user_orgs.iter().any(|org| org.id == org_id) {
return Ok(true);
}
}
Ok(false)
}
/// Checks if a user has access to a metric through any associated collection.
///
/// This function is used to implement permission cascading from collections to metrics.
/// If a user has access to any collection containing the metric (either through direct permissions,
/// workspace sharing, or if the collection is public), they get at least CanView permission.
///
/// # Arguments
/// * `metric_id` - UUID of the metric to check
/// * `user_id` - UUID of the user to check permissions for
/// * `user_orgs` - User's organization memberships
///
/// # Returns
/// * `Result<bool>` - True if the user has access to any collection containing the metric, false otherwise
pub async fn check_metric_collection_access(
metric_id: &Uuid,
user_id: &Uuid,
user_orgs: &[OrganizationMembership],
) -> Result<bool, diesel::result::Error> {
let mut conn = get_pg_pool().get().await.map_err(|e| {
diesel::result::Error::DatabaseError(
diesel::result::DatabaseErrorKind::UnableToSendCommand,
Box::new(e.to_string()),
)
})?;
// First check if user has direct access to any collection containing this metric
let has_direct_access = collections_to_assets::table
.inner_join(
collections::table
.on(collections::id.eq(collections_to_assets::collection_id)),
)
.inner_join(
asset_permissions::table.on(
asset_permissions::asset_id.eq(collections::id)
.and(asset_permissions::asset_type.eq(AssetType::Collection))
.and(asset_permissions::identity_id.eq(user_id))
.and(asset_permissions::identity_type.eq(IdentityType::User))
.and(asset_permissions::deleted_at.is_null())
),
)
.filter(collections_to_assets::asset_id.eq(metric_id))
.filter(collections_to_assets::asset_type.eq(AssetType::MetricFile))
.filter(collections::deleted_at.is_null())
.filter(collections_to_assets::deleted_at.is_null())
.select(collections::id)
.first::<Uuid>(&mut conn)
.await
.optional()?;
if has_direct_access.is_some() {
return Ok(true);
}
// Note: Collections don't have publicly_accessible fields, only workspace_sharing
// Check if metric belongs to any workspace-shared collection
let workspace_shared_collection = collections_to_assets::table
.inner_join(
collections::table
.on(collections::id.eq(collections_to_assets::collection_id)),
)
.filter(collections_to_assets::asset_id.eq(metric_id))
.filter(collections_to_assets::asset_type.eq(AssetType::MetricFile))
.filter(collections::deleted_at.is_null())
.filter(collections_to_assets::deleted_at.is_null())
.filter(collections::workspace_sharing.ne(WorkspaceSharing::None))
.select((collections::organization_id, collections::workspace_sharing))
.first::<(Uuid, WorkspaceSharing)>(&mut conn)
.await
.optional()?;
if let Some((org_id, _sharing_level)) = workspace_shared_collection {
// Check if user is member of that organization
if user_orgs.iter().any(|org| org.id == org_id) {
return Ok(true);
}
}
Ok(false)
}
/// Checks if a user has access to a dashboard through any associated collection.
///
/// This function is used to implement permission cascading from collections to dashboards.
/// If a user has access to any collection containing the dashboard (either through direct permissions,
/// workspace sharing, or if the collection is public), they get at least CanView permission.
///
/// # Arguments
/// * `dashboard_id` - UUID of the dashboard to check
/// * `user_id` - UUID of the user to check permissions for
/// * `user_orgs` - User's organization memberships
///
/// # Returns
/// * `Result<bool>` - True if the user has access to any collection containing the dashboard, false otherwise
pub async fn check_dashboard_collection_access(
dashboard_id: &Uuid,
user_id: &Uuid,
user_orgs: &[OrganizationMembership],
) -> Result<bool, diesel::result::Error> {
let mut conn = get_pg_pool().get().await.map_err(|e| {
diesel::result::Error::DatabaseError(
diesel::result::DatabaseErrorKind::UnableToSendCommand,
Box::new(e.to_string()),
)
})?;
// First check if user has direct access to any collection containing this dashboard
let has_direct_access = collections_to_assets::table
.inner_join(
collections::table
.on(collections::id.eq(collections_to_assets::collection_id)),
)
.inner_join(
asset_permissions::table.on(
asset_permissions::asset_id.eq(collections::id)
.and(asset_permissions::asset_type.eq(AssetType::Collection))
.and(asset_permissions::identity_id.eq(user_id))
.and(asset_permissions::identity_type.eq(IdentityType::User))
.and(asset_permissions::deleted_at.is_null())
),
)
.filter(collections_to_assets::asset_id.eq(dashboard_id))
.filter(collections_to_assets::asset_type.eq(AssetType::DashboardFile))
.filter(collections::deleted_at.is_null())
.filter(collections_to_assets::deleted_at.is_null())
.select(collections::id)
.first::<Uuid>(&mut conn)
.await
.optional()?;
if has_direct_access.is_some() {
return Ok(true);
}
// Note: Collections don't have publicly_accessible fields, only workspace_sharing
// Check if dashboard belongs to any workspace-shared collection
let workspace_shared_collection = collections_to_assets::table
.inner_join(
collections::table
.on(collections::id.eq(collections_to_assets::collection_id)),
)
.filter(collections_to_assets::asset_id.eq(dashboard_id))
.filter(collections_to_assets::asset_type.eq(AssetType::DashboardFile))
.filter(collections::deleted_at.is_null())
.filter(collections_to_assets::deleted_at.is_null())
.filter(collections::workspace_sharing.ne(WorkspaceSharing::None))
.select((collections::organization_id, collections::workspace_sharing))
.first::<(Uuid, WorkspaceSharing)>(&mut conn)
.await
.optional()?;
if let Some((org_id, _sharing_level)) = workspace_shared_collection {
// Check if user is member of that organization
if user_orgs.iter().any(|org| org.id == org_id) {
return Ok(true);
}
}
Ok(false)
}
#[cfg(test)]
@ -299,7 +578,8 @@ mod tests {
None,
&[AssetPermissionRole::Owner],
org_id,
&grants
&grants,
WorkspaceSharing::None
));
}
@ -315,7 +595,8 @@ mod tests {
None,
&[AssetPermissionRole::Owner],
org_id,
&grants
&grants,
WorkspaceSharing::None
));
}
@ -331,7 +612,8 @@ mod tests {
Some(AssetPermissionRole::CanEdit),
&[AssetPermissionRole::CanEdit],
org_id,
&grants
&grants,
WorkspaceSharing::None
));
}
@ -347,7 +629,8 @@ mod tests {
Some(AssetPermissionRole::CanView),
&[AssetPermissionRole::CanEdit],
org_id,
&grants
&grants,
WorkspaceSharing::None
));
}
@ -363,7 +646,8 @@ mod tests {
None,
&[AssetPermissionRole::CanView],
org_id,
&grants
&grants,
WorkspaceSharing::None
));
}
}

View File

@ -60,6 +60,7 @@ mod tests {
use axum::routing::post;
use axum::Router;
use chrono::Utc;
use database::enums::WorkspaceSharing;
use database::{
enums::{AssetPermissionRole, AssetType, IdentityType, UserOrganizationRole},
models::{AssetPermission, Chat, Message, MessageToFile, User},
@ -137,6 +138,9 @@ mod tests {
most_recent_file_id: None,
most_recent_file_type: None,
most_recent_version_number: None,
workspace_sharing: WorkspaceSharing::None,
workspace_sharing_enabled_at: None,
workspace_sharing_enabled_by: None,
};
let mut conn = get_pg_pool().get().await.unwrap();
@ -184,6 +188,8 @@ mod tests {
deleted_at: None,
created_by: user.id,
feedback: None,
is_completed: true,
post_processing_message: None,
};
insert_into(messages::table)

View File

@ -5,6 +5,7 @@ import {
getReactions,
getThreadMessages,
removeReaction,
convertMarkdownToSlack,
} from '@buster/slack';
import { type TaskOutput, logger, runs, schemaTask, wait } from '@trigger.dev/sdk';
import { z } from 'zod';
@ -546,32 +547,43 @@ export const slackAgentTask: ReturnType<
buttonUrl = `${busterUrl}/app/chats/${payload.chatId}/${chatFileInfo.mostRecentFileType}s/${chatFileInfo.mostRecentFileId}?${chatFileInfo.mostRecentFileType}_version_number=${chatFileInfo.mostRecentVersionNumber}`;
}
const completionMessage = {
text: responseText,
thread_ts: chatDetails.slackThreadTs,
blocks: [
{
type: 'section' as const,
text: {
type: 'mrkdwn' as const,
text: responseText,
},
// Convert markdown to Slack format
const convertedResponse = convertMarkdownToSlack(responseText);
// Create the message with converted text and any blocks from conversion
const messageBlocks = [...(convertedResponse.blocks || [])];
// If no blocks were created from conversion, create a section block with the converted text
if (messageBlocks.length === 0 && convertedResponse.text) {
messageBlocks.push({
type: 'section' as const,
text: {
type: 'mrkdwn' as const,
text: convertedResponse.text,
},
});
}
// Add the action button block
messageBlocks.push({
type: 'actions' as const,
elements: [
{
type: 'actions' as const,
elements: [
{
type: 'button' as const,
text: {
type: 'plain_text' as const,
text: 'Open in Buster',
emoji: false,
},
url: buttonUrl,
},
],
type: 'button' as const,
text: {
type: 'plain_text' as const,
text: 'Open in Buster',
emoji: false,
},
url: buttonUrl,
},
],
});
const completionMessage = {
text: convertedResponse.text || responseText, // Use converted text as fallback
thread_ts: chatDetails.slackThreadTs,
blocks: messageBlocks,
};
await messagingService.sendMessage(

View File

@ -0,0 +1,17 @@
CREATE TYPE "public"."workspace_sharing_enum" AS ENUM('none', 'can_view', 'can_edit', 'full_access');--> statement-breakpoint
ALTER TABLE "chats" ADD COLUMN "workspace_sharing" "workspace_sharing_enum" DEFAULT 'none' NOT NULL;--> statement-breakpoint
ALTER TABLE "chats" ADD COLUMN "workspace_sharing_enabled_by" uuid;--> statement-breakpoint
ALTER TABLE "chats" ADD COLUMN "workspace_sharing_enabled_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "collections" ADD COLUMN "workspace_sharing" "workspace_sharing_enum" DEFAULT 'none' NOT NULL;--> statement-breakpoint
ALTER TABLE "collections" ADD COLUMN "workspace_sharing_enabled_by" uuid;--> statement-breakpoint
ALTER TABLE "collections" ADD COLUMN "workspace_sharing_enabled_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "dashboard_files" ADD COLUMN "workspace_sharing" "workspace_sharing_enum" DEFAULT 'none' NOT NULL;--> statement-breakpoint
ALTER TABLE "dashboard_files" ADD COLUMN "workspace_sharing_enabled_by" uuid;--> statement-breakpoint
ALTER TABLE "dashboard_files" ADD COLUMN "workspace_sharing_enabled_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "metric_files" ADD COLUMN "workspace_sharing" "workspace_sharing_enum" DEFAULT 'none' NOT NULL;--> statement-breakpoint
ALTER TABLE "metric_files" ADD COLUMN "workspace_sharing_enabled_by" uuid;--> statement-breakpoint
ALTER TABLE "metric_files" ADD COLUMN "workspace_sharing_enabled_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "chats" ADD CONSTRAINT "chats_workspace_sharing_enabled_by_fkey" FOREIGN KEY ("workspace_sharing_enabled_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "collections" ADD CONSTRAINT "collections_workspace_sharing_enabled_by_fkey" FOREIGN KEY ("workspace_sharing_enabled_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "dashboard_files" ADD CONSTRAINT "dashboard_files_workspace_sharing_enabled_by_fkey" FOREIGN KEY ("workspace_sharing_enabled_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "metric_files" ADD CONSTRAINT "metric_files_workspace_sharing_enabled_by_fkey" FOREIGN KEY ("workspace_sharing_enabled_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE cascade;

File diff suppressed because it is too large Load Diff

View File

@ -568,6 +568,13 @@
"when": 1752700439888,
"tag": "0080_famous_emma_frost",
"breakpoints": true
},
{
"idx": 81,
"version": "7",
"when": 1752770723373,
"tag": "0081_curly_silvermane",
"breakpoints": true
}
]
}

View File

@ -105,6 +105,13 @@ export const slackSharingPermissionEnum = pgEnum('slack_sharing_permission_enum'
'noSharing',
]);
export const workspaceSharingEnum = pgEnum('workspace_sharing_enum', [
'none',
'can_view',
'can_edit',
'full_access',
]);
export const apiKeys = pgTable(
'api_keys',
{
@ -258,6 +265,9 @@ export const collections = pgTable(
.notNull(),
deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }),
organizationId: uuid('organization_id').notNull(),
workspaceSharing: workspaceSharingEnum('workspace_sharing').default('none').notNull(),
workspaceSharingEnabledBy: uuid('workspace_sharing_enabled_by'),
workspaceSharingEnabledAt: timestamp('workspace_sharing_enabled_at', { withTimezone: true, mode: 'string' }),
},
(table) => [
foreignKey({
@ -275,6 +285,11 @@ export const collections = pgTable(
foreignColumns: [users.id],
name: 'collections_updated_by_fkey',
}).onUpdate('cascade'),
foreignKey({
columns: [table.workspaceSharingEnabledBy],
foreignColumns: [users.id],
name: 'collections_workspace_sharing_enabled_by_fkey',
}).onUpdate('cascade'),
]
);
@ -926,6 +941,9 @@ export const dashboardFiles = pgTable(
}),
versionHistory: jsonb('version_history').default({}).notNull(),
publicPassword: text('public_password'),
workspaceSharing: workspaceSharingEnum('workspace_sharing').default('none').notNull(),
workspaceSharingEnabledBy: uuid('workspace_sharing_enabled_by'),
workspaceSharingEnabledAt: timestamp('workspace_sharing_enabled_at', { withTimezone: true, mode: 'string' }),
},
(table) => [
index('dashboard_files_created_by_idx').using(
@ -950,6 +968,11 @@ export const dashboardFiles = pgTable(
foreignColumns: [users.id],
name: 'dashboard_files_publicly_enabled_by_fkey',
}).onUpdate('cascade'),
foreignKey({
columns: [table.workspaceSharingEnabledBy],
foreignColumns: [users.id],
name: 'dashboard_files_workspace_sharing_enabled_by_fkey',
}).onUpdate('cascade'),
]
);
@ -980,6 +1003,9 @@ export const chats = pgTable(
slackChatAuthorization: slackChatAuthorizationEnum('slack_chat_authorization'),
slackThreadTs: text('slack_thread_ts'),
slackChannelId: text('slack_channel_id'),
workspaceSharing: workspaceSharingEnum('workspace_sharing').default('none').notNull(),
workspaceSharingEnabledBy: uuid('workspace_sharing_enabled_by'),
workspaceSharingEnabledAt: timestamp('workspace_sharing_enabled_at', { withTimezone: true, mode: 'string' }),
},
(table) => [
index('chats_created_at_idx').using(
@ -1019,6 +1045,11 @@ export const chats = pgTable(
foreignColumns: [users.id],
name: 'chats_publicly_enabled_by_fkey',
}).onUpdate('cascade'),
foreignKey({
columns: [table.workspaceSharingEnabledBy],
foreignColumns: [users.id],
name: 'chats_workspace_sharing_enabled_by_fkey',
}).onUpdate('cascade'),
]
);
@ -1116,6 +1147,9 @@ export const metricFiles = pgTable(
dataMetadata: jsonb('data_metadata'),
publicPassword: text('public_password'),
dataSourceId: uuid('data_source_id').notNull(),
workspaceSharing: workspaceSharingEnum('workspace_sharing').default('none').notNull(),
workspaceSharingEnabledBy: uuid('workspace_sharing_enabled_by'),
workspaceSharingEnabledAt: timestamp('workspace_sharing_enabled_at', { withTimezone: true, mode: 'string' }),
},
(table) => [
index('metric_files_created_by_idx').using(
@ -1149,6 +1183,11 @@ export const metricFiles = pgTable(
foreignColumns: [dataSources.id],
name: 'fk_data_source',
}),
foreignKey({
columns: [table.workspaceSharingEnabledBy],
foreignColumns: [users.id],
name: 'metric_files_workspace_sharing_enabled_by_fkey',
}).onUpdate('cascade'),
]
);

View File

@ -39,6 +39,7 @@ export { MessageTrackingDataSchema } from './interfaces/message-tracking';
export * from './utils/validation-helpers';
export * from './utils/message-formatter';
export * from './utils/oauth-helpers';
export { convertMarkdownToSlack } from './utils/markdown-to-slack';
// Reactions
export { addReaction, removeReaction, getReactions } from './reactions';