Skip to content

Commit

Permalink
Implement query/discover permissions, fix bug related to read-only qu…
Browse files Browse the repository at this point in the history
…eries, surface read-only errors, cover with tests
  • Loading branch information
marcua committed Oct 28, 2024
1 parent 61438e8 commit 3caf241
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 76 deletions.
12 changes: 4 additions & 8 deletions src/ayb_db/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,30 +120,26 @@ impl AuthenticationMethodStatus {
#[repr(i16)]
pub enum PublicSharingLevel {
NoAccess = 0,
Metadata = 1,
Fork = 2,
ReadOnly = 3,
Fork = 1,
ReadOnly = 2,
}

from_str!(PublicSharingLevel, {
"no-access" => PublicSharingLevel::NoAccess,
"metadata" => PublicSharingLevel::Metadata,
"fork" => PublicSharingLevel::Fork,
"read-only" => PublicSharingLevel::ReadOnly
});

try_from_i16!(PublicSharingLevel, {
0 => PublicSharingLevel::NoAccess,
1 => PublicSharingLevel::Metadata,
2 => PublicSharingLevel::Fork,
3 => PublicSharingLevel::ReadOnly
1 => PublicSharingLevel::Fork,
2 => PublicSharingLevel::ReadOnly
});

impl PublicSharingLevel {
pub fn to_str(&self) -> &str {
match self {
PublicSharingLevel::NoAccess => "no-access",
PublicSharingLevel::Metadata => "metadata",
PublicSharingLevel::Fork => "fork",
PublicSharingLevel::ReadOnly => "read-only",
}
Expand Down
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use url;
#[serde(tag = "type")]
pub enum AybError {
DurationParseError { message: String },
NoWriteAccessError { message: String },
S3ExecutionError { message: String },
S3ConnectionError { message: String },
SnapshotError { message: String },
Expand All @@ -31,6 +32,7 @@ impl Display for AybError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
AybError::Other { message } => write!(f, "{}", message),
AybError::NoWriteAccessError { message } => write!(f, "{}", message),
_ => write!(f, "{:?}", self),
}
}
Expand Down
1 change: 1 addition & 0 deletions src/hosted_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::vec::Vec;

#[derive(Debug)]
#[repr(i16)]
pub enum QueryMode {
ReadOnly = 0,
Expand Down
28 changes: 19 additions & 9 deletions src/hosted_db/sqlite.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::error::AybError;
use crate::hosted_db::{sandbox::run_in_sandbox, QueryMode, QueryResult};
use crate::server::config::AybConfigIsolation;
use rusqlite;
use rusqlite::config::DbConfig;
use rusqlite::limits::Limit;
use rusqlite::types::ValueRef;
Expand All @@ -18,14 +19,14 @@ pub fn query_sqlite(
) -> Result<QueryResult, AybError> {
// The flags below are the default `open` flags in `rusqlite`
// except for `..READ_ONLY` and `..READ_WRITE`.
let mut open_flags = rusqlite::OpenFlags::SQLITE_OPEN_CREATE
| rusqlite::OpenFlags::SQLITE_OPEN_URI
| rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX;
open_flags = open_flags
| match query_mode {
QueryMode::ReadOnly => rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
QueryMode::ReadWrite => rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE,
};
let mut open_flags =
rusqlite::OpenFlags::SQLITE_OPEN_URI | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX;
open_flags |= match query_mode {
QueryMode::ReadOnly => rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
QueryMode::ReadWrite => {
rusqlite::OpenFlags::SQLITE_OPEN_CREATE | rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE
}
};
let conn = rusqlite::Connection::open_with_flags(path, open_flags)?;

if !allow_unsafe {
Expand All @@ -46,7 +47,16 @@ pub fn query_sqlite(

let mut rows = prepared.query([])?;
let mut results: Vec<Vec<Option<String>>> = Vec::new();
while let Some(row) = rows.next()? {
while let Some(row) = rows.next().map_err(|err| match err {
rusqlite::Error::SqliteFailure(ref code, _)
if code.code == rusqlite::ErrorCode::ReadOnly && code.extended_code == 8 =>
{
AybError::NoWriteAccessError {
message: "Attempted to write to a read-only database".to_string(),
}
}
_ => AybError::from(err),
})? {
let mut result: Vec<Option<String>> = Vec::new();
for column_index in 0..num_columns {
let column_value = row.get_ref(column_index)?;
Expand Down
19 changes: 8 additions & 11 deletions src/server/endpoints/entity_details.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
use crate::ayb_db::db_interfaces::AybDb;
use crate::ayb_db::models::InstantiatedEntity;
use crate::error::AybError;
use crate::http::structs::{
EntityDatabase, EntityPath, EntityProfile, EntityProfileLink, EntityQueryResponse,
};
use crate::server::permissions::can_query;
use crate::http::structs::{EntityPath, EntityProfile, EntityProfileLink, EntityQueryResponse};
use crate::server::permissions::can_discover_database;
use crate::server::utils::unwrap_authenticated_entity;
use actix_web::{get, web};

Expand All @@ -18,13 +16,12 @@ pub async fn entity_details(
let entity_slug = &path.entity.to_lowercase();
let desired_entity = ayb_db.get_entity_by_slug(entity_slug).await?;

let databases = ayb_db
.list_databases_by_entity(&desired_entity)
.await?
.into_iter()
.filter(|v| can_query(&authenticated_entity, v))
.map(From::from)
.collect::<Vec<EntityDatabase>>();
let mut databases = Vec::new();
for database in ayb_db.list_databases_by_entity(&desired_entity).await? {
if can_discover_database(&authenticated_entity, &database)? {
databases.push(database.into());
}
}

let links: Vec<EntityProfileLink> = desired_entity.links.map_or_else(Vec::new, |l| {
l.iter()
Expand Down
37 changes: 19 additions & 18 deletions src/server/endpoints/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ use crate::ayb_db::models::{DBType, InstantiatedEntity};

use crate::error::AybError;
use crate::hosted_db::paths::current_database_path;
use crate::hosted_db::{run_query, QueryMode, QueryResult};
use crate::hosted_db::{run_query, QueryResult};
use crate::http::structs::EntityDatabasePath;
use crate::server::config::AybConfig;
use crate::server::permissions::can_query;
use crate::server::permissions::highest_query_access_level;
use crate::server::utils::unwrap_authenticated_entity;
use actix_web::{post, web};

Expand All @@ -23,25 +23,26 @@ async fn query(
let database = ayb_db.get_database(entity_slug, database_slug).await?;
let authenticated_entity = unwrap_authenticated_entity(&authenticated_entity)?;

if can_query(&authenticated_entity, &database) {
let db_type = DBType::try_from(database.db_type)?;
let db_path = current_database_path(entity_slug, database_slug, &ayb_config.data_path)?;
// TODO(marcua): Determine read-only or read-write
let result = run_query(
&db_path,
&query,
&db_type,
&ayb_config.isolation,
QueryMode::ReadWrite,
)
.await?;
Ok(web::Json(result))
} else {
Err(AybError::Other {
let access_level = highest_query_access_level(&authenticated_entity, &database)?;
match access_level {
Some(access_level) => {
let db_type = DBType::try_from(database.db_type)?;
let db_path = current_database_path(entity_slug, database_slug, &ayb_config.data_path)?;
let result = run_query(
&db_path,
&query,
&db_type,
&ayb_config.isolation,
access_level,
)
.await?;
Ok(web::Json(result))
}
None => Err(AybError::Other {
message: format!(
"Authenticated entity {} can't query database {}/{}",
authenticated_entity.slug, entity_slug, database_slug
),
})
}),
}
}
37 changes: 30 additions & 7 deletions src/server/permissions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
use crate::ayb_db::models::{InstantiatedDatabase, InstantiatedEntity};
use crate::ayb_db::models::{InstantiatedDatabase, InstantiatedEntity, PublicSharingLevel};
use crate::error::AybError;
use crate::hosted_db::QueryMode;

fn is_owner(authenticated_entity: &InstantiatedEntity, database: &InstantiatedDatabase) -> bool {
authenticated_entity.id == database.entity_id
}

pub fn can_create_database(
authenticated_entity: &InstantiatedEntity,
Expand All @@ -8,26 +14,43 @@ pub fn can_create_database(
authenticated_entity.id == desired_entity.id
}

pub fn can_discover_database(
authenticated_entity: &InstantiatedEntity,
database: &InstantiatedDatabase,
) -> Result<bool, AybError> {
let public_sharing_level = PublicSharingLevel::try_from(database.public_sharing_level)?;
Ok(is_owner(authenticated_entity, database)
|| public_sharing_level == PublicSharingLevel::ReadOnly
|| public_sharing_level == PublicSharingLevel::Fork)
}

pub fn can_manage_database(
authenticated_entity: &InstantiatedEntity,
database: &InstantiatedDatabase,
) -> bool {
// An entity/user can only manage its own databases (for now)
authenticated_entity.id == database.entity_id
is_owner(authenticated_entity, database)
}

pub fn can_query(
pub fn highest_query_access_level(
authenticated_entity: &InstantiatedEntity,
database: &InstantiatedDatabase,
) -> bool {
// An entity/user can only query its own databases (for now)
authenticated_entity.id == database.entity_id
) -> Result<Option<QueryMode>, AybError> {
if is_owner(authenticated_entity, database) {
Ok(Some(QueryMode::ReadWrite))
} else if PublicSharingLevel::try_from(database.public_sharing_level)?
== PublicSharingLevel::ReadOnly
{
Ok(Some(QueryMode::ReadOnly))
} else {
Ok(None)
}
}

pub fn can_manage_snapshots(
authenticated_entity: &InstantiatedEntity,
database: &InstantiatedDatabase,
) -> bool {
// An entity/user can only manage snapshots on its own databases (for now)
authenticated_entity.id == database.entity_id
is_owner(authenticated_entity, database)
}
4 changes: 2 additions & 2 deletions src/server/snapshots/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ pub async fn snapshot_database(
// Run in unsafe mode to allow backup process to
// attach to destination database.
true,
QueryMode::ReadWrite,
QueryMode::ReadOnly,
)?;
if !result.rows.is_empty() {
return Err(AybError::SnapshotError {
Expand All @@ -176,7 +176,7 @@ pub async fn snapshot_database(
&snapshot_path,
"PRAGMA integrity_check;",
false,
QueryMode::ReadWrite,
QueryMode::ReadOnly,
)?;
if result.fields.len() != 1
|| result.rows.len() != 1
Expand Down
Loading

0 comments on commit 3caf241

Please sign in to comment.