mirror of https://github.com/buster-so/buster.git
ok get metric has permissions on it now
This commit is contained in:
parent
ed60e7ebd9
commit
65840319fa
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use database::types::VersionHistory;
|
use database::types::VersionHistory;
|
||||||
use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper};
|
use diesel::{ExpressionMethods, JoinOnDsl, QueryDsl, Queryable, Selectable, SelectableHelper};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use serde_yaml;
|
use serde_yaml;
|
||||||
|
@ -9,9 +9,9 @@ use uuid::Uuid;
|
||||||
use crate::metrics::types::{
|
use crate::metrics::types::{
|
||||||
BusterMetric, ColumnMetaData, ColumnType, DataMetadata, Dataset, MinMaxValue, SimpleType,
|
BusterMetric, ColumnMetaData, ColumnType, DataMetadata, Dataset, MinMaxValue, SimpleType,
|
||||||
};
|
};
|
||||||
use database::enums::{AssetPermissionRole, Verification};
|
use database::enums::{AssetPermissionRole, AssetType, IdentityType, Verification};
|
||||||
use database::pool::get_pg_pool;
|
use database::pool::get_pg_pool;
|
||||||
use database::schema::{datasets, metric_files};
|
use database::schema::{asset_permissions, datasets, metric_files, users};
|
||||||
use database::types::MetricYml;
|
use database::types::MetricYml;
|
||||||
|
|
||||||
use super::Version;
|
use super::Version;
|
||||||
|
@ -31,6 +31,9 @@ struct QueryableMetricFile {
|
||||||
created_at: chrono::DateTime<chrono::Utc>,
|
created_at: chrono::DateTime<chrono::Utc>,
|
||||||
updated_at: chrono::DateTime<chrono::Utc>,
|
updated_at: chrono::DateTime<chrono::Utc>,
|
||||||
version_history: VersionHistory,
|
version_history: VersionHistory,
|
||||||
|
publicly_accessible: bool,
|
||||||
|
publicly_enabled_by: Option<Uuid>,
|
||||||
|
public_expiry_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable)]
|
#[derive(Queryable)]
|
||||||
|
@ -39,20 +42,34 @@ struct DatasetInfo {
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable)]
|
#[derive(Queryable, Selectable)]
|
||||||
#[diesel(table_name = users)]
|
#[diesel(table_name = users)]
|
||||||
struct UserInfo {
|
struct UserInfo {
|
||||||
|
id: Uuid,
|
||||||
|
email: String,
|
||||||
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
|
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
|
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
|
||||||
avatar_url: Option<String>,
|
avatar_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Queryable)]
|
||||||
|
struct AssetPermissionInfo {
|
||||||
|
identity_id: Uuid,
|
||||||
|
role: AssetPermissionRole,
|
||||||
|
email: String,
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Handler to retrieve a metric by ID with optional version number
|
/// Handler to retrieve a metric by ID with optional version number
|
||||||
///
|
///
|
||||||
/// If version_number is provided, returns that specific version of the metric.
|
/// If version_number is provided, returns that specific version of the metric.
|
||||||
/// If version_number is None, returns the latest version of the metric.
|
/// If version_number is None, returns the latest version of the metric.
|
||||||
pub async fn get_metric_handler(metric_id: &Uuid, user_id: &Uuid, version_number: Option<i32>) -> Result<BusterMetric> {
|
pub async fn get_metric_handler(
|
||||||
|
metric_id: &Uuid,
|
||||||
|
user_id: &Uuid,
|
||||||
|
version_number: Option<i32>,
|
||||||
|
) -> Result<BusterMetric> {
|
||||||
let mut conn = match get_pg_pool().get().await {
|
let mut conn = match get_pg_pool().get().await {
|
||||||
Ok(conn) => conn,
|
Ok(conn) => conn,
|
||||||
Err(e) => return Err(anyhow!("Failed to get database connection: {}", e)),
|
Err(e) => return Err(anyhow!("Failed to get database connection: {}", e)),
|
||||||
|
@ -75,6 +92,9 @@ pub async fn get_metric_handler(metric_id: &Uuid, user_id: &Uuid, version_number
|
||||||
metric_files::created_at,
|
metric_files::created_at,
|
||||||
metric_files::updated_at,
|
metric_files::updated_at,
|
||||||
metric_files::version_history,
|
metric_files::version_history,
|
||||||
|
metric_files::publicly_accessible,
|
||||||
|
metric_files::publicly_enabled_by,
|
||||||
|
metric_files::public_expiry_date,
|
||||||
))
|
))
|
||||||
.first::<QueryableMetricFile>(&mut conn)
|
.first::<QueryableMetricFile>(&mut conn)
|
||||||
.await
|
.await
|
||||||
|
@ -99,8 +119,10 @@ pub async fn get_metric_handler(metric_id: &Uuid, user_id: &Uuid, version_number
|
||||||
// Get the specific version if it exists
|
// Get the specific version if it exists
|
||||||
if let Some(v) = metric_file.version_history.get_version(version) {
|
if let Some(v) = metric_file.version_history.get_version(version) {
|
||||||
match &v.content {
|
match &v.content {
|
||||||
database::types::VersionContent::MetricYml(content) => (content.clone(), v.version_number),
|
database::types::VersionContent::MetricYml(content) => {
|
||||||
_ => return Err(anyhow!("Invalid version content type"))
|
(content.clone(), v.version_number)
|
||||||
|
}
|
||||||
|
_ => return Err(anyhow!("Invalid version content type")),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow!("Version {} not found", version));
|
return Err(anyhow!("Version {} not found", version));
|
||||||
|
@ -109,8 +131,10 @@ pub async fn get_metric_handler(metric_id: &Uuid, user_id: &Uuid, version_number
|
||||||
// Get the latest version
|
// Get the latest version
|
||||||
if let Some(v) = metric_file.version_history.get_latest_version() {
|
if let Some(v) = metric_file.version_history.get_latest_version() {
|
||||||
match &v.content {
|
match &v.content {
|
||||||
database::types::VersionContent::MetricYml(content) => (content.clone(), v.version_number),
|
database::types::VersionContent::MetricYml(content) => {
|
||||||
_ => return Err(anyhow!("Invalid version content type"))
|
(content.clone(), v.version_number)
|
||||||
|
}
|
||||||
|
_ => return Err(anyhow!("Invalid version content type")),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fall back to current content if no version history
|
// Fall back to current content if no version history
|
||||||
|
@ -193,6 +217,55 @@ pub async fn get_metric_handler(metric_id: &Uuid, user_id: &Uuid, version_number
|
||||||
// Sort versions by version_number in ascending order
|
// Sort versions by version_number in ascending order
|
||||||
versions.sort_by(|a, b| a.version_number.cmp(&b.version_number));
|
versions.sort_by(|a, b| a.version_number.cmp(&b.version_number));
|
||||||
|
|
||||||
|
// Query individual permissions for this metric
|
||||||
|
let individual_permissions_query = asset_permissions::table
|
||||||
|
.inner_join(users::table.on(users::id.eq(asset_permissions::identity_id)))
|
||||||
|
.filter(asset_permissions::asset_id.eq(metric_id))
|
||||||
|
.filter(asset_permissions::asset_type.eq(AssetType::MetricFile))
|
||||||
|
.filter(asset_permissions::identity_type.eq(IdentityType::User))
|
||||||
|
.filter(asset_permissions::deleted_at.is_null())
|
||||||
|
.select((
|
||||||
|
asset_permissions::identity_id,
|
||||||
|
asset_permissions::role,
|
||||||
|
users::email,
|
||||||
|
users::name,
|
||||||
|
))
|
||||||
|
.load::<AssetPermissionInfo>(&mut conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Get the user info for publicly_enabled_by if it exists
|
||||||
|
let public_enabled_by_user = if let Some(enabled_by_id) = metric_file.publicly_enabled_by {
|
||||||
|
users::table
|
||||||
|
.filter(users::id.eq(enabled_by_id))
|
||||||
|
.select(users::email)
|
||||||
|
.first::<String>(&mut conn)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert AssetPermissionInfo to BusterShareIndividual
|
||||||
|
let individual_permissions = match individual_permissions_query {
|
||||||
|
Ok(permissions) => {
|
||||||
|
if permissions.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
permissions
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| crate::metrics::types::BusterShareIndividual {
|
||||||
|
email: p.email,
|
||||||
|
role: p.role,
|
||||||
|
name: p.name,
|
||||||
|
})
|
||||||
|
.collect::<Vec<crate::metrics::types::BusterShareIndividual>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
// Construct BusterMetric
|
// Construct BusterMetric
|
||||||
Ok(BusterMetric {
|
Ok(BusterMetric {
|
||||||
id: metric_file.id,
|
id: metric_file.id,
|
||||||
|
@ -223,5 +296,11 @@ pub async fn get_metric_handler(metric_id: &Uuid, user_id: &Uuid, version_number
|
||||||
// TODO: get the actual access check
|
// TODO: get the actual access check
|
||||||
permission: AssetPermissionRole::Owner,
|
permission: AssetPermissionRole::Owner,
|
||||||
sql: metric_content.sql,
|
sql: metric_content.sql,
|
||||||
|
// New sharing fields
|
||||||
|
individual_permissions,
|
||||||
|
publicly_accessible: metric_file.publicly_accessible,
|
||||||
|
public_expiry_date: metric_file.public_expiry_date,
|
||||||
|
public_enabled_by: public_enabled_by_user,
|
||||||
|
public_password: None, // Currently not stored in the database
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,13 @@ pub struct Version {
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct BusterShareIndividual {
|
||||||
|
pub email: String,
|
||||||
|
pub role: AssetPermissionRole,
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct BusterMetric {
|
pub struct BusterMetric {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
@ -47,6 +54,12 @@ pub struct BusterMetric {
|
||||||
pub versions: Vec<Version>,
|
pub versions: Vec<Version>,
|
||||||
pub permission: AssetPermissionRole,
|
pub permission: AssetPermissionRole,
|
||||||
pub sql: String,
|
pub sql: String,
|
||||||
|
// Sharing fields
|
||||||
|
pub individual_permissions: Option<Vec<BusterShareIndividual>>,
|
||||||
|
pub public_expiry_date: Option<DateTime<Utc>>,
|
||||||
|
pub public_enabled_by: Option<String>,
|
||||||
|
pub publicly_accessible: bool,
|
||||||
|
pub public_password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::common::{
|
||||||
|
env::{create_env, TestEnv},
|
||||||
|
http::client::TestClient,
|
||||||
|
assertions::response::assert_api_ok,
|
||||||
|
};
|
||||||
|
use chrono::Utc;
|
||||||
|
use database::enums::{AssetPermissionRole, AssetTypeEnum, IdentityTypeEnum};
|
||||||
|
use diesel::{ExpressionMethods, QueryDsl};
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_metric_with_sharing_info() {
|
||||||
|
// Setup test environment
|
||||||
|
let env = create_env().await;
|
||||||
|
let client = TestClient::new(&env);
|
||||||
|
|
||||||
|
// Create test user and metric
|
||||||
|
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
|
||||||
|
let metric_id = create_test_metric(&env, user_id).await;
|
||||||
|
|
||||||
|
// Add sharing permissions
|
||||||
|
add_test_permissions(&env, metric_id, user_id).await;
|
||||||
|
|
||||||
|
// Add public sharing
|
||||||
|
enable_public_sharing(&env, metric_id, user_id).await;
|
||||||
|
|
||||||
|
// Test GET request
|
||||||
|
let response = client
|
||||||
|
.get(&format!("/api/v1/metrics/{}", metric_id))
|
||||||
|
.header("X-User-Id", user_id.to_string())
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Assert success and verify response
|
||||||
|
let data = assert_api_ok(response).await;
|
||||||
|
|
||||||
|
// Check fields
|
||||||
|
assert_eq!(data["id"], metric_id.to_string());
|
||||||
|
assert_eq!(data["type"], "metric");
|
||||||
|
|
||||||
|
// Check sharing fields
|
||||||
|
assert_eq!(data["publicly_accessible"], true);
|
||||||
|
assert!(data["public_expiry_date"].is_string());
|
||||||
|
assert_eq!(data["public_enabled_by"], "test@example.com");
|
||||||
|
assert_eq!(data["individual_permissions"].as_array().unwrap().len(), 1);
|
||||||
|
|
||||||
|
let permission = &data["individual_permissions"][0];
|
||||||
|
assert_eq!(permission["email"], "test2@example.com");
|
||||||
|
assert_eq!(permission["role"], "viewer");
|
||||||
|
assert_eq!(permission["name"], "Test User 2");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions to set up the test data
|
||||||
|
async fn create_test_metric(env: &TestEnv, user_id: Uuid) -> Uuid {
|
||||||
|
let mut conn = env.db_pool.get().await.unwrap();
|
||||||
|
|
||||||
|
// Insert test user
|
||||||
|
diesel::sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING")
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(user_id)
|
||||||
|
.bind::<diesel::sql_types::Text, _>("test@example.com")
|
||||||
|
.bind::<diesel::sql_types::Text, _>("Test User")
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Insert another test user
|
||||||
|
let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap();
|
||||||
|
diesel::sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING")
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(user2_id)
|
||||||
|
.bind::<diesel::sql_types::Text, _>("test2@example.com")
|
||||||
|
.bind::<diesel::sql_types::Text, _>("Test User 2")
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Insert test metric
|
||||||
|
let metric_id = Uuid::parse_str("00000000-0000-0000-0000-000000000010").unwrap();
|
||||||
|
let org_id = Uuid::parse_str("00000000-0000-0000-0000-000000000100").unwrap();
|
||||||
|
|
||||||
|
// Insert test organization if needed
|
||||||
|
diesel::sql_query("INSERT INTO organizations (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING")
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(org_id)
|
||||||
|
.bind::<diesel::sql_types::Text, _>("Test Organization")
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Insert metric
|
||||||
|
diesel::sql_query(r#"
|
||||||
|
INSERT INTO metric_files (id, name, file_name, content, verification, organization_id, created_by, version_history)
|
||||||
|
VALUES ($1, 'Test Metric', 'test.yml', '{"description": "Test description", "time_frame": "daily", "dataset_ids": [], "chart_config": {}, "sql": "SELECT 1;"}', 'notRequested', $2, $3, '{}'::jsonb)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
"#)
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(metric_id)
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(org_id)
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(user_id)
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
metric_id
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_test_permissions(env: &TestEnv, metric_id: Uuid, user_id: Uuid) {
|
||||||
|
let mut conn = env.db_pool.get().await.unwrap();
|
||||||
|
|
||||||
|
// Get the second user
|
||||||
|
let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap();
|
||||||
|
|
||||||
|
// Add permission for user2 as viewer
|
||||||
|
diesel::sql_query(r#"
|
||||||
|
INSERT INTO asset_permissions (identity_id, identity_type, asset_id, asset_type, role, created_by, updated_by)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $6)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
"#)
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(user2_id)
|
||||||
|
.bind::<diesel::sql_types::Text, _>(IdentityTypeEnum::User.to_string())
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(metric_id)
|
||||||
|
.bind::<diesel::sql_types::Text, _>(AssetTypeEnum::MetricFile.to_string())
|
||||||
|
.bind::<diesel::sql_types::Text, _>(AssetPermissionRole::CanView.to_string())
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(user_id)
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn enable_public_sharing(env: &TestEnv, metric_id: Uuid, user_id: Uuid) {
|
||||||
|
let mut conn = env.db_pool.get().await.unwrap();
|
||||||
|
|
||||||
|
// Set public access
|
||||||
|
let expiry_date = Utc::now() + chrono::Duration::days(7);
|
||||||
|
|
||||||
|
diesel::sql_query(r#"
|
||||||
|
UPDATE metric_files
|
||||||
|
SET publicly_accessible = true, publicly_enabled_by = $1, public_expiry_date = $2
|
||||||
|
WHERE id = $3
|
||||||
|
"#)
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(user_id)
|
||||||
|
.bind::<diesel::sql_types::Timestamptz, _>(expiry_date)
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(metric_id)
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
|
@ -2,4 +2,5 @@
|
||||||
pub mod update_metric_test;
|
pub mod update_metric_test;
|
||||||
pub mod delete_metric_test;
|
pub mod delete_metric_test;
|
||||||
pub mod post_metric_dashboard_test;
|
pub mod post_metric_dashboard_test;
|
||||||
|
pub mod get_metric_test;
|
||||||
pub mod sharing;
|
pub mod sharing;
|
Loading…
Reference in New Issue