mirror of https://github.com/buster-so/buster.git
update workspace sharing on all assets
This commit is contained in:
parent
217dc9ca9d
commit
a0a1e11493
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ pub async fn delete_chats_handler(
|
|||
],
|
||||
cwp.chat.organization_id,
|
||||
&user.organizations,
|
||||
cwp.chat.workspace_sharing,
|
||||
);
|
||||
|
||||
has_permission
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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'),
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue