pub fn statement_writes(sql: &str) -> bool {
let Some(first) = leading_keyword(sql) else {
return true;
};
!matches!(
first.as_str(),
"SELECT" | "WITH" | "SHOW" | "EXPLAIN" | "DESCRIBE" | "DESC" | "PRAGMA" | "VALUES"
)
}
fn leading_keyword(sql: &str) -> Option<String> {
let mut rest = sql.trim_start();
loop {
if let Some(after) = rest.strip_prefix("--") {
rest = after.find('\n').map_or("", |at| &after[at + 1..]).trim_start();
continue;
}
if let Some(after) = rest.strip_prefix("/*") {
rest = after.find("*/").map_or("", |at| &after[at + 2..]).trim_start();
continue;
}
if let Some(after) = rest.strip_prefix('(') {
rest = after.trim_start();
continue;
}
break;
}
let word: String = rest
.chars()
.take_while(|c| c.is_ascii_alphabetic())
.collect();
(!word.is_empty()).then(|| word.to_ascii_uppercase())
}
pub fn capability_for_statement(sql: &str) -> crate::platform::model::ProjectCapability {
use crate::platform::model::ProjectCapability;
if statement_writes(sql) {
ProjectCapability::TablesWrite
} else {
ProjectCapability::TablesRead
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_are_recognised_as_reads() {
for sql in [
"SELECT 1",
" select * from posts",
"WITH t AS (SELECT 1) SELECT * FROM t",
"(SELECT 1)",
"-- a comment about DELETE\nSELECT 1",
"/* DROP TABLE posts */ SELECT 1",
"EXPLAIN SELECT 1",
"PRAGMA table_info(posts)",
] {
assert!(!statement_writes(sql), "expected a read: {sql:?}");
}
}
#[test]
fn writes_are_recognised_as_writes() {
for sql in [
"DELETE FROM posts",
"delete from posts",
"INSERT INTO posts VALUES (1)",
"UPDATE posts SET title = 'x'",
"DROP TABLE posts",
"ALTER TABLE posts ADD COLUMN x TEXT",
"TRUNCATE posts",
"GRANT ALL ON posts TO someone",
"-- looks harmless\nDELETE FROM posts",
] {
assert!(statement_writes(sql), "expected a write: {sql:?}");
}
}
#[test]
fn a_write_statement_needs_the_write_capability() {
use crate::platform::model::{ProjectAccessRolePreset, ProjectCapability};
use crate::platform::services::access::roles::role_capabilities;
assert_eq!(
capability_for_statement("DELETE FROM posts"),
ProjectCapability::TablesWrite
);
assert_eq!(
capability_for_statement("SELECT * FROM posts"),
ProjectCapability::TablesRead
);
let reporter = role_capabilities(ProjectAccessRolePreset::Reporter);
assert!(reporter.contains(&ProjectCapability::TablesRead));
assert!(
!reporter.contains(&ProjectCapability::TablesWrite),
"a Reporter holding TablesWrite would make the statement check pointless"
);
let developer = role_capabilities(ProjectAccessRolePreset::Developer);
assert!(!developer.contains(&ProjectCapability::TablesWrite));
}
#[test]
fn anything_unrecognised_counts_as_a_write() {
for sql in ["", " ", ";;;", "42", "🙂"] {
assert!(statement_writes(sql), "expected a write: {sql:?}");
}
}
}