Skip to content

Commit

Permalink
feat: support volume restrictions on file:// urls, in-mem SQLite DBs
Browse files Browse the repository at this point in the history
  • Loading branch information
jsoverson committed Aug 28, 2023
1 parent 105b080 commit 4516bb7
Show file tree
Hide file tree
Showing 14 changed files with 338 additions and 40 deletions.
4 changes: 4 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ _wick-db-tests:
{{wick}} test ./examples/db/tests/postgres-numeric-tests.wick
{{wick}} test ./examples/db/tests/postgres-null-tests.wick
{{wick}} test ./examples/db/tests/postgres-date-tests.wick
{{wick}} test ./examples/db/postgres-component.wick
{{wick}} test ./examples/db/azuresql-component.wick
{{wick}} test ./examples/db/sqlite-component.wick
{{wick}} test ./examples/db/sqlite-inmemory-component.wick
{{wick}} test ./tests/cli-tests/tests/cmd/db/azuresql-tx-test.wick

# Run `wick` tests for http components
Expand Down
14 changes: 5 additions & 9 deletions crates/components/wick-sql/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,11 @@ impl Client {
normalize_operations(config.operations_mut(), DbKind::Postgres);
Arc::new(crate::sqlx::SqlXComponent::new(config.clone(), resolver).await?)
}
"sqlite" => {
"file" | "sqlite" => {
normalize_operations(config.operations_mut(), DbKind::Sqlite);

Arc::new(crate::sqlx::SqlXComponent::new(config.clone(), resolver).await?)
}

_ => {
return Err(Error::InvalidScheme(url.scheme().to_owned()));
}
_ => return Err(Error::InvalidScheme(url.scheme().to_owned())),
};

Ok(Self { inner: client })
Expand Down Expand Up @@ -440,7 +436,7 @@ fn normalize_inline_ids(orig_query: &str, mut orig_args: Vec<String>) -> (Cow<st
let id = id_map.get(id).unwrap();
format!("${}", id)
});
debug!(%orig_query,%normalized, "sql:mssql:normalized query");
debug!(%orig_query,%normalized, "sql:inline-replacement");
(normalized, orig_args)
} else {
(Cow::Borrowed(orig_query), orig_args)
Expand Down Expand Up @@ -646,7 +642,7 @@ mod integration_test {
let mut app_config = wick_config::config::AppConfiguration::default();
app_config.add_resource(
"db",
ResourceDefinition::Url(format!("sqlite://{}", db).try_into().unwrap()),
ResourceDefinition::Url(format!("file://{}", db).try_into().unwrap()),
);

let component = SqlComponent::new(config, None, None, &app_config.resolver()).await?;
Expand All @@ -658,7 +654,7 @@ mod integration_test {
async fn test_sqlite_basic() -> Result<()> {
let pg = init_sqlite_component().await?;
let input = packet_stream!(("input", 1_i32));
let inv = Invocation::test("postgres", "wick://__local__/test", input, None)?;
let inv = Invocation::test("sqlite", "wick://__local__/test", input, None)?;
let response = pg.handle(inv, Default::default(), panic_callback()).await.unwrap();
let packets: Vec<_> = response.collect().await;

Expand Down
5 changes: 5 additions & 0 deletions crates/components/wick-sql/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ pub enum Error {
#[error("Unknown database scheme '{0}'")]
InvalidScheme(String),

#[error(
"To use in-memory SQLite databases, use the URL 'sqlite://memory'; to use a SQLite DB file, use a 'file://' URL"
)]
SqliteScheme,

#[error("Failed to prepare arguments: {0}")]
Prepare(String),

Expand Down
20 changes: 19 additions & 1 deletion crates/components/wick-sql/src/sqlx/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,26 @@ fn validate(config: &SqlComponentConfig, _resolver: &Resolver) -> Result<(), Err

async fn init_client(config: &SqlComponentConfig, addr: &Url) -> Result<CtxPool, Error> {
let pool = match addr.scheme() {
"sqlite" => CtxPool::SqlLite(sqlite::connect(config, addr).await?),
"file" => CtxPool::SqlLite(
sqlite::connect(
config,
Some(
addr
.to_file_path()
.map_err(|_e| Error::SqliteConnect(format!("could not convert url {} to filepath", addr)))?
.to_str()
.unwrap(),
),
)
.await?,
),
"postgres" => CtxPool::Postgres(postgres::connect(config, addr).await?),
"sqlite" => {
if addr.host() != Some(url::Host::Domain("memory")) {
return Err(Error::SqliteScheme);
}
CtxPool::SqlLite(sqlite::connect(config, None).await?)
}
"mysql" => unimplemented!("MySql is not supported yet"),
"mssql" => unreachable!(),
s => return Err(Error::InvalidScheme(s.to_owned())),
Expand Down
6 changes: 3 additions & 3 deletions crates/components/wick-sql/src/sqlx/sqlite.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
mod serialize;
use sqlx::sqlite::SqlitePoolOptions;
use sqlx::{Sqlite, SqlitePool};
use url::Url;
use wick_config::config::components::SqlComponentConfig;

pub(crate) use self::serialize::*;
use crate::common::sql_wrapper::ConvertedType;
use crate::Error;

pub(crate) async fn connect(_config: &SqlComponentConfig, addr: &Url) -> Result<SqlitePool, Error> {
pub(crate) async fn connect(_config: &SqlComponentConfig, addr: Option<&str>) -> Result<SqlitePool, Error> {
let addr = addr.unwrap_or(":memory:");
debug!(%addr, "connecting to sqlite");

let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect(addr.as_ref())
.connect(addr)
.await
.map_err(|e| Error::SqliteConnect(e.to_string()))?;
Ok(pool)
Expand Down
2 changes: 1 addition & 1 deletion crates/components/wick-sql/src/sqlx/sqlite/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ mod integration_test {

async fn connect() -> SqliteConnection {
let db = std::env::var("SQLITE_DB").unwrap();
let conn_string = format!("sqlite://{}", db);
let conn_string = format!("file://{}", db);

SqliteConnection::connect(&conn_string).await.unwrap()
}
Expand Down
132 changes: 122 additions & 10 deletions crates/wick/wick-config/src/lockdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod volume;

pub use error::*;

use crate::audit::{Audit, AuditedResource, AuditedResourceBinding};
use crate::audit::{Audit, AuditedResource, AuditedResourceBinding, AuditedVolume};
use crate::config::{ConfigOrDefinition, LockdownConfiguration, ResourceRestriction};

pub(crate) fn validate_resource(
Expand Down Expand Up @@ -33,15 +33,40 @@ pub(crate) fn validate_resource(
_ => None,
}),
),
AuditedResource::Url(v) => self::url::validate(
component_id,
&resource.name,
v,
resource_restrictions.iter().filter_map(|r| match r {
ResourceRestriction::Url(v) => Some(v),
_ => None,
}),
),
AuditedResource::Url(v) => {
if v.url.scheme() == "file" {
let Ok(path) = v.url.to_file_path() else {
return Err(LockdownError::new(vec![FailureKind::FileUrlInvalid(v.url.clone())]));
};
if !path.exists() {
return Err(LockdownError::new(vec![FailureKind::FileUrlNotFound(v.url.clone())]));
}
let dir = if path.is_file() {
path.parent().map(std::path::Path::to_path_buf).unwrap()
} else {
path
};
self::volume::validate(
component_id,
&resource.name,
&AuditedVolume { path: dir },
resource_restrictions.iter().filter_map(|r| match r {
ResourceRestriction::Volume(v) => Some(v),
_ => None,
}),
)
} else {
self::url::validate(
component_id,
&resource.name,
v,
resource_restrictions.iter().filter_map(|r| match r {
ResourceRestriction::Url(v) => Some(v),
_ => None,
}),
)
}
}
AuditedResource::Volume(v) => self::volume::validate(
component_id,
&resource.name,
Expand Down Expand Up @@ -78,10 +103,12 @@ pub trait Lockdown {
mod test {
use std::path::PathBuf;

use ::url::Url;
use anyhow::Result;
use normpath::PathExt;

use super::*;
use crate::audit::AuditedUrl;
use crate::config::{
components,
AppConfigurationBuilder,
Expand All @@ -92,6 +119,7 @@ mod test {
ImportDefinition,
LockdownConfigurationBuilder,
ResourceRestriction,
UrlRestriction,
VolumeRestriction,
};
use crate::WickConfiguration;
Expand Down Expand Up @@ -124,6 +152,90 @@ mod test {
)
}

fn pwdify(s: impl Into<String>) -> String {
s.into().replace("$CRATE", env!("CARGO_MANIFEST_DIR"))
}

fn path(path: impl Into<String>) -> PathBuf {
PathBuf::from(pwdify(path))
}

fn url(path: impl Into<String>) -> Url {
pwdify(path).parse().unwrap()
}

fn mktmpfile(file: impl Into<String>) -> String {
let file = std::env::temp_dir().join(file.into());
std::fs::write(&file, "test contents").unwrap();
file.to_string_lossy().to_string()
}

#[rstest::rstest]
#[case("$CRATE", "file:///$CRATE/DOES_NOT_EXIST")]
#[case("$CRATE", "file://$CRATE/DOES_NOT_EXIST")]
#[case("$CRATE", "file:///$CRATE/../../../README.md")]
#[case("$CRATE", "file://$CRATE/../../../README.md")]
#[case("$CRATE", "file:/$CRATE/../../../README.md")]
#[case("$CRATE", format!("file:///{}",mktmpfile("TEST.md")))]
fn test_file_url_volume_restriction_fails(
#[case] allowed_path: impl Into<String>,
#[case] resource_url: impl Into<String>,
) -> Result<()> {
let allowed_path = path(allowed_path);
let resource_url = url(resource_url);
let file_url = AuditedResourceBinding {
name: "url".to_owned(),
resource: AuditedResource::Url(AuditedUrl { url: resource_url }),
};

// Allow all URLs for "test_component" but have a volume restriction that should fail.
let lockdown = new_lockdown_config(vec![
ResourceRestriction::Url(UrlRestriction::new_from_template(vec!["test_component".into()], "*")),
ResourceRestriction::Volume(VolumeRestriction::new_from_template(
vec!["test_component".into()],
allowed_path.to_string_lossy(),
)),
]);

let result = validate_resource("test_component", &file_url, &lockdown);
if let Err(e) = result {
println!("{}", e);
} else {
panic!("Expected an error, got {:?}", result);
}

Ok(())
}

#[rstest::rstest]
#[case("$CRATE", "file:///$CRATE/README.md")]
#[case("$CRATE", "https://google.com")]
#[case("$CRATE", "postgres://pg:pg@127.0.0.1:5432")]
fn test_file_url_volume_restriction_passes(
#[case] allowed_path: impl Into<String>,
#[case] resource_url: impl Into<String>,
) -> Result<()> {
let allowed_path = path(allowed_path);
let resource_url = url(resource_url);
let file_url = AuditedResourceBinding {
name: "url".to_owned(),
resource: AuditedResource::Url(AuditedUrl { url: resource_url }),
};

// Allow all URLs for "test_component" but have a volume restriction that should fail.
let lockdown = new_lockdown_config(vec![
ResourceRestriction::Url(UrlRestriction::new_from_template(vec!["test_component".into()], "*")),
ResourceRestriction::Volume(VolumeRestriction::new_from_template(
vec!["test_component".into()],
allowed_path.to_string_lossy(),
)),
]);

validate_resource("test_component", &file_url, &lockdown)?;

Ok(())
}

#[test_logger::test(tokio::test)]
async fn test_tree_walker() -> Result<()> {
let mut config = AppConfigurationBuilder::default();
Expand Down
6 changes: 6 additions & 0 deletions crates/wick/wick-config/src/lockdown/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ pub enum FailureKind {
Address(String, String),
/// A component is not allowed to access given url.
Url(String, String),
/// A file:// URL could not be turned into a filepath.
FileUrlInvalid(url::Url),
/// A file:// URL does not point to a concrete file.
FileUrlNotFound(url::Url),
}

impl std::fmt::Display for FailureKind {
Expand All @@ -80,6 +84,8 @@ impl std::fmt::Display for FailureKind {
FailureKind::Port(id, port) => write!(f, "component {} is not allowed to access {}", id, port),
FailureKind::Address(id,address) => write!(f, "component {} is not allowed to access {}", id, address),
FailureKind::Url(id, url) => write!(f, "component {} is not allowed to access {}", id, url),
FailureKind::FileUrlInvalid(url) => write!(f, "could not create a file path out of {}", url),
FailureKind::FileUrlNotFound(url) => write!(f, "file URL '{}' does not point to a valid file", url),
}
}
}
2 changes: 1 addition & 1 deletion crates/wick/wick-config/src/lockdown/url.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#![allow(dead_code)]
use std::collections::HashSet;

use wildmatch::WildMatch;
Expand All @@ -15,6 +14,7 @@ pub(crate) fn validate<'a>(
restrictions: impl Iterator<Item = &'a UrlRestriction>,
) -> Result<(), LockdownError> {
let mut failures = HashSet::new();

for restriction in restrictions {
match is_allowed(component_id, resource_id, resource, restriction) {
Ok(_) => return Ok(()),
Expand Down
29 changes: 21 additions & 8 deletions examples/db/azuresql-component.wick
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ component:
type: string
- name: email
type: string
query: INSERT INTO users(name, email) OUTPUT INSERTED.* VALUES ($1, $2)
arguments:
- name
- email
query: INSERT INTO users(name, email) OUTPUT INSERTED.* VALUES (${name}, ${email})
- name: set_user_with_id
inputs:
- name: id
Expand All @@ -42,14 +39,30 @@ component:
type: string
- name: email
type: string
query: INSERT INTO users(id, name, email) OUTPUT INSERTED.* VALUES ($1, $2, $3)
arguments:
- name
- email
query: INSERT INTO users(id, name, email) OUTPUT INSERTED.* VALUES (${id}, ${name}, ${email})
- name: set_user_with_columns
inputs:
- name: input
type: string[]
query: INSERT INTO users(name, email) OUTPUT INSERTED.* VALUES ($1, $2)
arguments:
- input... # This special `spread` syntax expands the input array into individual positional arguments
tests:
- with:
password: '{{ctx.env.TEST_PASSWORD}}'
host: '{{ctx.env.TEST_HOST}}'
port: '{{ctx.env.MSSQL_PORT}}'
cases:
- operation: set_user
inputs:
- name: name
value: TEST_NAME
- name: email
value: TEST_EMAIL@example.com
outputs:
- name: output
assertions:
- operator: Contains
value:
email: TEST_EMAIL@example.com
name: TEST_NAME
Loading

0 comments on commit 4516bb7

Please sign in to comment.