From 2e61c39d0ae0f5e5cbc1ea2968db40b4a69f7ecf Mon Sep 17 00:00:00 2001 From: dal Date: Wed, 5 Mar 2025 17:52:08 -0700 Subject: [PATCH] add in sharing lib --- api/libs/sharing/Cargo.toml | 21 +++++ api/libs/sharing/src/errors.rs | 22 +++++ api/libs/sharing/src/lib.rs | 69 ++++++++++++++++ api/libs/sharing/src/models/mod.rs | 71 ++++++++++++++++ api/libs/sharing/src/utils/mod.rs | 127 +++++++++++++++++++++++++++++ 5 files changed, 310 insertions(+) create mode 100644 api/libs/sharing/Cargo.toml create mode 100644 api/libs/sharing/src/errors.rs create mode 100644 api/libs/sharing/src/lib.rs create mode 100644 api/libs/sharing/src/models/mod.rs create mode 100644 api/libs/sharing/src/utils/mod.rs diff --git a/api/libs/sharing/Cargo.toml b/api/libs/sharing/Cargo.toml new file mode 100644 index 000000000..ad2b24ba8 --- /dev/null +++ b/api/libs/sharing/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "sharing" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +chrono = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +uuid = { workspace = true } +diesel = { workspace = true } +diesel-async = { workspace = true } + +[dev-dependencies] +tokio-test = { workspace = true } + +[features] +default = [] \ No newline at end of file diff --git a/api/libs/sharing/src/errors.rs b/api/libs/sharing/src/errors.rs new file mode 100644 index 000000000..f955670ce --- /dev/null +++ b/api/libs/sharing/src/errors.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Resource not found: {0}")] + ResourceNotFound(String), + + #[error("Permission denied: {0}")] + PermissionDenied(String), + + #[error("Invalid sharing configuration: {0}")] + InvalidConfiguration(String), + + #[error("Resource already shared with user: {0}")] + AlreadyShared(String), + + #[error(transparent)] + Database(#[from] diesel::result::Error), + + #[error(transparent)] + Other(#[from] anyhow::Error), +} \ No newline at end of file diff --git a/api/libs/sharing/src/lib.rs b/api/libs/sharing/src/lib.rs new file mode 100644 index 000000000..fdad05912 --- /dev/null +++ b/api/libs/sharing/src/lib.rs @@ -0,0 +1,69 @@ +//! Sharing Library +//! +//! This library provides functionality for managing sharing of various resources +//! such as metrics, dashboards, collections, and chats. It handles the creation, +//! verification, and management of sharing records. + +use anyhow::Result; + +pub mod models; +pub mod utils; +mod errors; + +// Re-exports +pub use errors::Error; +pub use models::Share; +pub use utils::{create_share, check_access, remove_share, list_shares}; + +/// Represents the different types of resources that can be shared +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum ShareableResource { + /// A metric resource + Metric(uuid::Uuid), + /// A dashboard resource + Dashboard(uuid::Uuid), + /// A collection resource + Collection(uuid::Uuid), + /// A chat resource + Chat(uuid::Uuid), +} + +/// Represents the different levels of sharing permissions +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SharePermission { + /// Read-only access + View, + /// Can modify the resource + Edit, + /// Full control over the resource + Admin, +} + +/// A trait for types that can be shared +pub trait Shareable { + /// Convert the type into a ShareableResource + fn into_shareable(&self) -> ShareableResource; +} + +#[cfg(test)] +mod tests { + use super::*; + use uuid::Uuid; + + #[test] + fn test_shareable_resource_serialization() { + let id = Uuid::new_v4(); + let resource = ShareableResource::Metric(id); + let serialized = serde_json::to_string(&resource).unwrap(); + let deserialized: ShareableResource = serde_json::from_str(&serialized).unwrap(); + assert_eq!(resource, deserialized); + } + + #[test] + fn test_share_permission_serialization() { + let permission = SharePermission::Edit; + let serialized = serde_json::to_string(&permission).unwrap(); + let deserialized: SharePermission = serde_json::from_str(&serialized).unwrap(); + assert_eq!(permission, deserialized); + } +} \ No newline at end of file diff --git a/api/libs/sharing/src/models/mod.rs b/api/libs/sharing/src/models/mod.rs new file mode 100644 index 000000000..896652c43 --- /dev/null +++ b/api/libs/sharing/src/models/mod.rs @@ -0,0 +1,71 @@ +use chrono::{DateTime, Utc}; +use diesel::prelude::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{ShareableResource, SharePermission}; + +#[derive(Debug, Clone, Queryable, Insertable, Serialize, Deserialize)] +#[diesel(table_name = sharing)] +pub struct Share { + pub id: Uuid, + pub resource_type: String, + pub resource_id: Uuid, + pub shared_by: Uuid, + pub shared_with: Uuid, + pub permission: String, + pub created_at: DateTime, + pub updated_at: DateTime, + pub expires_at: Option>, +} + +impl Share { + pub fn new( + resource: ShareableResource, + shared_by: Uuid, + shared_with: Uuid, + permission: SharePermission, + expires_at: Option>, + ) -> Self { + Self { + id: Uuid::new_v4(), + resource_type: match &resource { + ShareableResource::Metric(_) => "metric", + ShareableResource::Dashboard(_) => "dashboard", + ShareableResource::Collection(_) => "collection", + ShareableResource::Chat(_) => "chat", + }.to_string(), + resource_id: match resource { + ShareableResource::Metric(id) | + ShareableResource::Dashboard(id) | + ShareableResource::Collection(id) | + ShareableResource::Chat(id) => id, + }, + shared_by, + shared_with, + permission: match permission { + SharePermission::View => "view", + SharePermission::Edit => "edit", + SharePermission::Admin => "admin", + }.to_string(), + created_at: Utc::now(), + updated_at: Utc::now(), + expires_at, + } + } +} + +// Diesel schema definition +table! { + sharing (id) { + id -> Uuid, + resource_type -> Text, + resource_id -> Uuid, + shared_by -> Uuid, + shared_with -> Uuid, + permission -> Text, + created_at -> Timestamptz, + updated_at -> Timestamptz, + expires_at -> Nullable, + } +} \ No newline at end of file diff --git a/api/libs/sharing/src/utils/mod.rs b/api/libs/sharing/src/utils/mod.rs new file mode 100644 index 000000000..7df4decbb --- /dev/null +++ b/api/libs/sharing/src/utils/mod.rs @@ -0,0 +1,127 @@ +use anyhow::Result; +use diesel::prelude::*; +use diesel_async::RunQueryDsl; +use uuid::Uuid; + +use crate::{ + models::Share, + ShareableResource, + SharePermission, + Error, +}; + +/// Creates a new sharing record +pub async fn create_share( + conn: &mut diesel_async::AsyncPgConnection, + resource: ShareableResource, + shared_by: Uuid, + shared_with: Uuid, + permission: SharePermission, +) -> Result { + use crate::models::sharing::dsl::*; + + // Check if sharing already exists + let existing = sharing + .filter(resource_id.eq(match &resource { + ShareableResource::Metric(id) | + ShareableResource::Dashboard(id) | + ShareableResource::Collection(id) | + ShareableResource::Chat(id) => id, + })) + .filter(shared_with.eq(shared_with)) + .first::(conn) + .await + .optional()?; + + if existing.is_some() { + return Err(Error::AlreadyShared("Resource already shared with this user".to_string()).into()); + } + + let share = Share::new(resource, shared_by, shared_with, permission, None); + diesel::insert_into(sharing) + .values(&share) + .get_result(conn) + .await + .map_err(Into::into) +} + +/// Checks if a user has access to a resource +pub async fn check_access( + conn: &mut diesel_async::AsyncPgConnection, + resource: &ShareableResource, + user_id: Uuid, + required_permission: SharePermission, +) -> Result { + use crate::models::sharing::dsl::*; + + let resource_id_val = match resource { + ShareableResource::Metric(id) | + ShareableResource::Dashboard(id) | + ShareableResource::Collection(id) | + ShareableResource::Chat(id) => *id, + }; + + let share = sharing + .filter(resource_id.eq(resource_id_val)) + .filter(shared_with.eq(user_id)) + .first::(conn) + .await + .optional()?; + + Ok(match share { + Some(share) => { + match (share.permission.as_str(), required_permission) { + (_, SharePermission::View) => true, + ("admin", _) => true, + ("edit", SharePermission::Edit) => true, + _ => false, + } + } + None => false, + }) +} + +/// Removes a sharing record +pub async fn remove_share( + conn: &mut diesel_async::AsyncPgConnection, + resource: &ShareableResource, + shared_with: Uuid, +) -> Result { + use crate::models::sharing::dsl::*; + + let resource_id_val = match resource { + ShareableResource::Metric(id) | + ShareableResource::Dashboard(id) | + ShareableResource::Collection(id) | + ShareableResource::Chat(id) => *id, + }; + + let deleted = diesel::delete(sharing) + .filter(resource_id.eq(resource_id_val)) + .filter(shared_with.eq(shared_with)) + .execute(conn) + .await?; + + Ok(deleted > 0) +} + +/// Lists all shares for a resource +pub async fn list_shares( + conn: &mut diesel_async::AsyncPgConnection, + resource: &ShareableResource, +) -> Result> { + use crate::models::sharing::dsl::*; + + let resource_id_val = match resource { + ShareableResource::Metric(id) | + ShareableResource::Dashboard(id) | + ShareableResource::Collection(id) | + ShareableResource::Chat(id) => *id, + }; + + sharing + .filter(resource_id.eq(resource_id_val)) + .load::(conn) + .await + .map_err(Into::into) +} \ No newline at end of file