@feltdb/core 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +19 -0
- package/dist/create/server-source/crates/feltdb/src/authority.rs +349 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +32 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +19 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +444 -189
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +244 -51
- package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +297 -0
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +41 -11
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +8 -3
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +1164 -225
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +134 -0
- package/dist/http-db.d.ts +3 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +2 -1
- package/dist/state-contract.d.ts +4 -1
- package/dist/state-contract.d.ts.map +1 -1
- package/dist/state-contract.js +1 -1
- package/dist/studio-app/assets/{feltdb_wasm-bIqcRzAr.js → feltdb_wasm-DB8cX151.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-ClhDHp0S.wasm +0 -0
- package/dist/studio-app/assets/{index-XGYdlElN.js → index-B0k4UAlI.js} +1 -1
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-BE79okwX.wasm +0 -0
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
|
|
23
23
|
use crate::{
|
|
24
24
|
application::{ApplicationManifest, ApplicationRevision, FieldDefinition},
|
|
25
|
-
policy_evaluation::{
|
|
25
|
+
policy_evaluation::{
|
|
26
|
+
Actor, AuthorizationState, PolicyEvaluator, PolicySubject, RecordAuthorizationContext,
|
|
27
|
+
},
|
|
26
28
|
AtomicMutation, AtomicPrecondition, FeltDb, StoredRow,
|
|
27
29
|
};
|
|
28
30
|
use serde::{Deserialize, Serialize};
|
|
@@ -616,7 +618,9 @@ pub fn begin_read(
|
|
|
616
618
|
"authorization context does not match schema",
|
|
617
619
|
));
|
|
618
620
|
}
|
|
619
|
-
let snapshot = db
|
|
621
|
+
let snapshot = db
|
|
622
|
+
.state_rows_for_namespace(state_namespace)
|
|
623
|
+
.map_err(StateFailure::storage)?;
|
|
620
624
|
Ok(ReadContext {
|
|
621
625
|
application_id: schema.application_id.clone(),
|
|
622
626
|
revision_id: schema.revision_id.clone(),
|
|
@@ -1344,11 +1348,17 @@ pub fn execute_query(
|
|
|
1344
1348
|
.collect::<Vec<_>>();
|
|
1345
1349
|
|
|
1346
1350
|
if let Some(policy) = &read_policy {
|
|
1347
|
-
let actor =
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1351
|
+
let actor =
|
|
1352
|
+
if context.authorization.subject.is_empty() || context.authorization.subject == ":" {
|
|
1353
|
+
None
|
|
1354
|
+
} else {
|
|
1355
|
+
context
|
|
1356
|
+
.authorization
|
|
1357
|
+
.subject
|
|
1358
|
+
.split(':')
|
|
1359
|
+
.nth(1)
|
|
1360
|
+
.map(|id| Actor::new(id))
|
|
1361
|
+
};
|
|
1352
1362
|
|
|
1353
1363
|
// Create authorization state for policies that need to verify relationships
|
|
1354
1364
|
let auth_state = AuthorizationState::new(
|
|
@@ -1367,8 +1377,7 @@ pub fn execute_query(
|
|
|
1367
1377
|
authorization_state: Some(auth_state.clone()),
|
|
1368
1378
|
};
|
|
1369
1379
|
|
|
1370
|
-
PolicyEvaluator::evaluate_record(policy.clone(), &record_context)
|
|
1371
|
-
.is_ok()
|
|
1380
|
+
PolicyEvaluator::evaluate_record(policy.clone(), &record_context).is_ok()
|
|
1372
1381
|
} else {
|
|
1373
1382
|
true
|
|
1374
1383
|
}
|
|
@@ -1618,6 +1627,8 @@ pub struct TransactionRequest {
|
|
|
1618
1627
|
#[serde(default)]
|
|
1619
1628
|
pub state_namespace: Option<String>,
|
|
1620
1629
|
#[serde(default)]
|
|
1630
|
+
/// Optimistic authority revision. A mismatched revision is rejected.
|
|
1631
|
+
#[serde(rename = "base_revision")]
|
|
1621
1632
|
pub causal_parent: Option<u64>,
|
|
1622
1633
|
pub authorization: AuthorizationContext,
|
|
1623
1634
|
pub operations: Vec<TransactionOperation>,
|
|
@@ -1642,6 +1653,9 @@ pub struct TransactionAudit {
|
|
|
1642
1653
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
1643
1654
|
pub struct TransactionResult {
|
|
1644
1655
|
pub transaction_id: String,
|
|
1656
|
+
pub base_revision: u64,
|
|
1657
|
+
pub commit_revision: u64,
|
|
1658
|
+
pub status: crate::TransactionStatus,
|
|
1645
1659
|
pub state_before: u64,
|
|
1646
1660
|
pub state_after: u64,
|
|
1647
1661
|
pub causal_cursor: BTreeMap<String, u64>,
|
|
@@ -1899,7 +1913,7 @@ fn validate_record(collection: &CollectionSchema, value: &mut Value) -> Result<(
|
|
|
1899
1913
|
}
|
|
1900
1914
|
if object
|
|
1901
1915
|
.keys()
|
|
1902
|
-
.any(|name| !collection.fields.iter().any(|f| &f.name == name))
|
|
1916
|
+
.any(|name| name != "__version" && !collection.fields.iter().any(|f| &f.name == name))
|
|
1903
1917
|
{
|
|
1904
1918
|
return Err(StateFailure::new(
|
|
1905
1919
|
"VALIDATION_FAILED",
|
|
@@ -1972,13 +1986,42 @@ fn execute_transaction_with_policies(
|
|
|
1972
1986
|
.transaction_id
|
|
1973
1987
|
.clone()
|
|
1974
1988
|
.unwrap_or_else(|| format!("tx_{:x}", Sha256::digest(&canonical)));
|
|
1989
|
+
// Client transaction identities are application-local. The managed store
|
|
1990
|
+
// serves many applications from one durable log, so using the bare client
|
|
1991
|
+
// id as the substrate id lets one application collide with another.
|
|
1992
|
+
// Hash the authorization boundary into the private storage identity while
|
|
1993
|
+
// preserving the caller's id in every public result and audit record.
|
|
1994
|
+
let storage_transaction_id = format!(
|
|
1995
|
+
"txscope_{:x}_{}",
|
|
1996
|
+
Sha256::digest(
|
|
1997
|
+
format!(
|
|
1998
|
+
"{}\0{}\0{}",
|
|
1999
|
+
request.tenant_id,
|
|
2000
|
+
request.application_id,
|
|
2001
|
+
request
|
|
2002
|
+
.state_namespace
|
|
2003
|
+
.as_deref()
|
|
2004
|
+
.unwrap_or(&request.application_id)
|
|
2005
|
+
)
|
|
2006
|
+
.as_bytes()
|
|
2007
|
+
),
|
|
2008
|
+
transaction_id
|
|
2009
|
+
);
|
|
1975
2010
|
let payload_hash = format!("sha256:{:x}", Sha256::digest(&canonical));
|
|
1976
2011
|
if db
|
|
1977
|
-
.has_applied_transaction(&
|
|
2012
|
+
.has_applied_transaction(&storage_transaction_id)
|
|
1978
2013
|
.map_err(StateFailure::storage)?
|
|
1979
2014
|
{
|
|
1980
|
-
if db
|
|
1981
|
-
|
|
2015
|
+
if db
|
|
2016
|
+
.applied_transaction_payload_hash(&storage_transaction_id)
|
|
2017
|
+
.map_err(StateFailure::storage)?
|
|
2018
|
+
.as_deref()
|
|
2019
|
+
!= Some(payload_hash.as_str())
|
|
2020
|
+
{
|
|
2021
|
+
return Err(StateFailure::new(
|
|
2022
|
+
"CONFLICT",
|
|
2023
|
+
"transaction ID was already committed with a different canonical payload",
|
|
2024
|
+
));
|
|
1982
2025
|
}
|
|
1983
2026
|
let state = db.sequence().map_err(StateFailure::storage)?;
|
|
1984
2027
|
let cursor = db
|
|
@@ -2002,6 +2045,9 @@ fn execute_transaction_with_policies(
|
|
|
2002
2045
|
};
|
|
2003
2046
|
return Ok(TransactionResult {
|
|
2004
2047
|
transaction_id,
|
|
2048
|
+
base_revision: db.current_revision().map_err(StateFailure::storage)?,
|
|
2049
|
+
commit_revision: db.current_revision().map_err(StateFailure::storage)?,
|
|
2050
|
+
status: crate::TransactionStatus::Committed,
|
|
2005
2051
|
state_before: state,
|
|
2006
2052
|
state_after: state,
|
|
2007
2053
|
causal_cursor: cursor,
|
|
@@ -2111,7 +2157,9 @@ fn execute_transaction_with_policies(
|
|
|
2111
2157
|
.unwrap_or_else(|| persisted.map(|row| Some(row.value.clone())).unwrap_or(None));
|
|
2112
2158
|
|
|
2113
2159
|
// Per-operation record authorization
|
|
2114
|
-
let write_policy = collection_write_policies
|
|
2160
|
+
let write_policy = collection_write_policies
|
|
2161
|
+
.and_then(|policies| policies.get(&operation.collection))
|
|
2162
|
+
.or(uniform_write_policy);
|
|
2115
2163
|
if let Some(policy) = write_policy {
|
|
2116
2164
|
let actor = if request.authorization.subject.is_empty()
|
|
2117
2165
|
|| request.authorization.subject == ":"
|
|
@@ -2135,9 +2183,9 @@ fn execute_transaction_with_policies(
|
|
|
2135
2183
|
TransactionOperationKind::Update | TransactionOperationKind::Delete => {
|
|
2136
2184
|
// For update/delete: authorize against EXISTING record (before changes)
|
|
2137
2185
|
// This prevents authorization bypass via ownership/org changes
|
|
2138
|
-
existing_value
|
|
2139
|
-
|
|
2140
|
-
|
|
2186
|
+
existing_value
|
|
2187
|
+
.clone()
|
|
2188
|
+
.ok_or_else(|| StateFailure::new("CONFLICT", "record does not exist"))?
|
|
2141
2189
|
}
|
|
2142
2190
|
};
|
|
2143
2191
|
|
|
@@ -2160,16 +2208,15 @@ fn execute_transaction_with_policies(
|
|
|
2160
2208
|
authorization_state: Some(auth_state),
|
|
2161
2209
|
};
|
|
2162
2210
|
|
|
2163
|
-
PolicyEvaluator::evaluate_record(policy.clone(), &record_context)
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
})?;
|
|
2211
|
+
PolicyEvaluator::evaluate_record(policy.clone(), &record_context).map_err(|_| {
|
|
2212
|
+
StateFailure::new(
|
|
2213
|
+
"AUTHORIZATION_DENIED",
|
|
2214
|
+
&format!(
|
|
2215
|
+
"record authorization failed for operation on {}:{}",
|
|
2216
|
+
operation.collection, key
|
|
2217
|
+
),
|
|
2218
|
+
)
|
|
2219
|
+
})?;
|
|
2173
2220
|
}
|
|
2174
2221
|
|
|
2175
2222
|
let value = match operation.kind {
|
|
@@ -2304,7 +2351,7 @@ fn execute_transaction_with_policies(
|
|
|
2304
2351
|
}
|
|
2305
2352
|
let commit = db
|
|
2306
2353
|
.apply_atomic_transaction_content_addressed(
|
|
2307
|
-
&
|
|
2354
|
+
&storage_transaction_id,
|
|
2308
2355
|
Some(&payload_hash),
|
|
2309
2356
|
request.causal_parent,
|
|
2310
2357
|
&preconditions,
|
|
@@ -2367,6 +2414,9 @@ fn execute_transaction_with_policies(
|
|
|
2367
2414
|
};
|
|
2368
2415
|
Ok(TransactionResult {
|
|
2369
2416
|
transaction_id,
|
|
2417
|
+
base_revision: commit.base_revision,
|
|
2418
|
+
commit_revision: commit.commit_revision,
|
|
2419
|
+
status: commit.status,
|
|
2370
2420
|
state_before: commit.state_before,
|
|
2371
2421
|
state_after: commit.state_after,
|
|
2372
2422
|
causal_cursor: cursor,
|
|
@@ -2609,7 +2659,9 @@ mod tests {
|
|
|
2609
2659
|
stale.transaction_id = Some("stale".into());
|
|
2610
2660
|
stale.causal_parent = Some(0);
|
|
2611
2661
|
assert_eq!(
|
|
2612
|
-
execute_transaction(&db, &schema, &stale, None)
|
|
2662
|
+
execute_transaction(&db, &schema, &stale, None)
|
|
2663
|
+
.unwrap_err()
|
|
2664
|
+
.code,
|
|
2613
2665
|
"CONFLICT"
|
|
2614
2666
|
);
|
|
2615
2667
|
}
|
|
@@ -2786,16 +2838,149 @@ mod tests {
|
|
|
2786
2838
|
}
|
|
2787
2839
|
#[test]
|
|
2788
2840
|
fn explicit_transaction_id_is_bound_to_canonical_payload_across_restart() {
|
|
2789
|
-
let path=std::env::temp_dir().join(format!(
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
let
|
|
2795
|
-
let
|
|
2841
|
+
let path = std::env::temp_dir().join(format!(
|
|
2842
|
+
"feltdb-content-id-{}-{}.log",
|
|
2843
|
+
std::process::id(),
|
|
2844
|
+
crate::now_ms()
|
|
2845
|
+
));
|
|
2846
|
+
let schema = schema();
|
|
2847
|
+
let request = TransactionRequest {
|
|
2848
|
+
transaction_id: Some("stable-client-id".into()),
|
|
2849
|
+
tenant_id: "tenant".into(),
|
|
2850
|
+
application_id: "app".into(),
|
|
2851
|
+
revision_id: "rev".into(),
|
|
2852
|
+
schema_version: 1,
|
|
2853
|
+
state_namespace: None,
|
|
2854
|
+
causal_parent: None,
|
|
2855
|
+
authorization: auth(),
|
|
2856
|
+
preconditions: vec![],
|
|
2857
|
+
operations: vec![TransactionOperation {
|
|
2858
|
+
kind: TransactionOperationKind::Insert,
|
|
2859
|
+
collection: "incidents".into(),
|
|
2860
|
+
id: "content-bound".into(),
|
|
2861
|
+
value: serde_json::json!({"title":"Original"}),
|
|
2862
|
+
if_version: None,
|
|
2863
|
+
}],
|
|
2864
|
+
};
|
|
2865
|
+
{
|
|
2866
|
+
let db = crate::open(&path).unwrap();
|
|
2867
|
+
assert!(
|
|
2868
|
+
!execute_transaction(&db, &schema, &request, None)
|
|
2869
|
+
.unwrap()
|
|
2870
|
+
.duplicate
|
|
2871
|
+
);
|
|
2872
|
+
assert!(
|
|
2873
|
+
execute_transaction(&db, &schema, &request, None)
|
|
2874
|
+
.unwrap()
|
|
2875
|
+
.duplicate
|
|
2876
|
+
);
|
|
2877
|
+
}
|
|
2878
|
+
let reopened = crate::open(&path).unwrap();
|
|
2879
|
+
assert!(
|
|
2880
|
+
execute_transaction(&reopened, &schema, &request, None)
|
|
2881
|
+
.unwrap()
|
|
2882
|
+
.duplicate
|
|
2883
|
+
);
|
|
2884
|
+
let mut changed = request.clone();
|
|
2885
|
+
changed.operations[0].value = serde_json::json!({"title":"Changed"});
|
|
2886
|
+
let failure = execute_transaction(&reopened, &schema, &changed, None).unwrap_err();
|
|
2887
|
+
assert_eq!(failure.code, "CONFLICT");
|
|
2888
|
+
assert!(failure.message.contains("different canonical payload"));
|
|
2796
2889
|
std::fs::remove_file(path).ok();
|
|
2797
2890
|
}
|
|
2798
2891
|
#[test]
|
|
2892
|
+
fn explicit_transaction_id_is_isolated_between_applications() {
|
|
2893
|
+
fn application(
|
|
2894
|
+
app: &str,
|
|
2895
|
+
namespace: &str,
|
|
2896
|
+
transaction: &str,
|
|
2897
|
+
record: &str,
|
|
2898
|
+
) -> (StateSchema, TransactionRequest) {
|
|
2899
|
+
let mut schema = schema();
|
|
2900
|
+
schema.application_id = app.into();
|
|
2901
|
+
let mut authorization = auth();
|
|
2902
|
+
authorization.application_id = app.into();
|
|
2903
|
+
let request = TransactionRequest {
|
|
2904
|
+
transaction_id: Some(transaction.into()),
|
|
2905
|
+
tenant_id: "tenant".into(),
|
|
2906
|
+
application_id: app.into(),
|
|
2907
|
+
revision_id: "rev".into(),
|
|
2908
|
+
schema_version: 1,
|
|
2909
|
+
state_namespace: Some(namespace.into()),
|
|
2910
|
+
causal_parent: None,
|
|
2911
|
+
authorization,
|
|
2912
|
+
preconditions: vec![],
|
|
2913
|
+
operations: vec![TransactionOperation {
|
|
2914
|
+
kind: TransactionOperationKind::Insert,
|
|
2915
|
+
collection: "incidents".into(),
|
|
2916
|
+
id: record.into(),
|
|
2917
|
+
value: serde_json::json!({"title":format!("{app}-{record}")}),
|
|
2918
|
+
if_version: None,
|
|
2919
|
+
}],
|
|
2920
|
+
};
|
|
2921
|
+
(schema, request)
|
|
2922
|
+
}
|
|
2923
|
+
for (name, order) in [
|
|
2924
|
+
(
|
|
2925
|
+
"a-then-b",
|
|
2926
|
+
vec![("app-a", "shared-1", "one"), ("app-b", "shared-1", "one")],
|
|
2927
|
+
),
|
|
2928
|
+
(
|
|
2929
|
+
"b-then-a",
|
|
2930
|
+
vec![("app-b", "shared-1", "one"), ("app-a", "shared-1", "one")],
|
|
2931
|
+
),
|
|
2932
|
+
(
|
|
2933
|
+
"interleaved",
|
|
2934
|
+
vec![
|
|
2935
|
+
("app-a", "shared-1", "one"),
|
|
2936
|
+
("app-b", "shared-1", "one"),
|
|
2937
|
+
("app-a", "shared-2", "two"),
|
|
2938
|
+
("app-b", "shared-2", "two"),
|
|
2939
|
+
],
|
|
2940
|
+
),
|
|
2941
|
+
] {
|
|
2942
|
+
let path = std::env::temp_dir().join(format!(
|
|
2943
|
+
"feltdb-app-isolation-{name}-{}-{}.log",
|
|
2944
|
+
std::process::id(),
|
|
2945
|
+
crate::now_ms()
|
|
2946
|
+
));
|
|
2947
|
+
{
|
|
2948
|
+
let db = crate::open(&path).unwrap();
|
|
2949
|
+
for (app, transaction, record) in &order {
|
|
2950
|
+
let (schema, request) =
|
|
2951
|
+
application(app, &format!("{app}:production"), transaction, record);
|
|
2952
|
+
assert!(
|
|
2953
|
+
!execute_transaction(&db, &schema, &request, None)
|
|
2954
|
+
.unwrap()
|
|
2955
|
+
.duplicate
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
let reopened = crate::open(&path).unwrap();
|
|
2960
|
+
for (app, transaction, record) in &order {
|
|
2961
|
+
let (schema, request) =
|
|
2962
|
+
application(app, &format!("{app}:production"), transaction, record);
|
|
2963
|
+
assert!(
|
|
2964
|
+
execute_transaction(&reopened, &schema, &request, None)
|
|
2965
|
+
.unwrap()
|
|
2966
|
+
.duplicate
|
|
2967
|
+
);
|
|
2968
|
+
let rows = reopened.state_rows().unwrap();
|
|
2969
|
+
assert_eq!(
|
|
2970
|
+
rows.iter()
|
|
2971
|
+
.filter(
|
|
2972
|
+
|row| row.capability == format!("{app}:production:incidents")
|
|
2973
|
+
&& row.key == *record
|
|
2974
|
+
&& !row.deleted
|
|
2975
|
+
)
|
|
2976
|
+
.count(),
|
|
2977
|
+
1
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
std::fs::remove_file(path).ok();
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
#[test]
|
|
2799
2984
|
fn read_context_remains_a_stable_snapshot() {
|
|
2800
2985
|
let db = db("snapshot-read");
|
|
2801
2986
|
let schema = schema();
|
|
@@ -2880,7 +3065,7 @@ mod tests {
|
|
|
2880
3065
|
}
|
|
2881
3066
|
let reopened = FeltDb::open(path).unwrap();
|
|
2882
3067
|
assert_eq!(reopened.export_snapshot().unwrap().rows.len(), 2);
|
|
2883
|
-
|
|
3068
|
+
assert_eq!(reopened.current_revision().unwrap(), 1);
|
|
2884
3069
|
}
|
|
2885
3070
|
#[test]
|
|
2886
3071
|
fn indexed_equality_query_uses_index_not_scan() {
|
|
@@ -3324,7 +3509,10 @@ mod tests {
|
|
|
3324
3509
|
};
|
|
3325
3510
|
|
|
3326
3511
|
let result = execute_query(&db, &schema, &context, &query, None);
|
|
3327
|
-
assert!(
|
|
3512
|
+
assert!(
|
|
3513
|
+
result.is_ok(),
|
|
3514
|
+
"Query should succeed with state:read capability"
|
|
3515
|
+
);
|
|
3328
3516
|
}
|
|
3329
3517
|
|
|
3330
3518
|
#[test]
|
|
@@ -3571,7 +3759,10 @@ mod tests {
|
|
|
3571
3759
|
let result = execute_query(&db, &schema, &context, &query, None);
|
|
3572
3760
|
assert!(result.is_ok(), "Authenticated read must be allowed");
|
|
3573
3761
|
let query_result = result.unwrap();
|
|
3574
|
-
assert!(
|
|
3762
|
+
assert!(
|
|
3763
|
+
query_result.records.len() >= 1,
|
|
3764
|
+
"Query should return the inserted record"
|
|
3765
|
+
);
|
|
3575
3766
|
}
|
|
3576
3767
|
|
|
3577
3768
|
#[test]
|
|
@@ -3612,7 +3803,10 @@ mod tests {
|
|
|
3612
3803
|
|
|
3613
3804
|
let result = execute_transaction(&db, &schema, &request, None);
|
|
3614
3805
|
assert!(result.is_ok(), "Authenticated write must be allowed");
|
|
3615
|
-
assert!(
|
|
3806
|
+
assert!(
|
|
3807
|
+
result.unwrap().state_after > 0,
|
|
3808
|
+
"Transaction should have succeeded"
|
|
3809
|
+
);
|
|
3616
3810
|
}
|
|
3617
3811
|
|
|
3618
3812
|
#[test]
|
|
@@ -3693,9 +3887,11 @@ mod tests {
|
|
|
3693
3887
|
references: vec![],
|
|
3694
3888
|
};
|
|
3695
3889
|
|
|
3696
|
-
let verify_result =
|
|
3890
|
+
let verify_result =
|
|
3891
|
+
execute_query(&db, &schema, &verify_context, &verify_query, None).unwrap();
|
|
3697
3892
|
assert_eq!(
|
|
3698
|
-
verify_result.records.len(),
|
|
3893
|
+
verify_result.records.len(),
|
|
3894
|
+
1,
|
|
3699
3895
|
"Only initial record should exist; denied transaction must not partially commit"
|
|
3700
3896
|
);
|
|
3701
3897
|
}
|
|
@@ -3810,7 +4006,10 @@ mod tests {
|
|
|
3810
4006
|
};
|
|
3811
4007
|
|
|
3812
4008
|
let result = execute_query(&db, &schema, &read_context, &query, None);
|
|
3813
|
-
assert!(
|
|
4009
|
+
assert!(
|
|
4010
|
+
result.is_ok(),
|
|
4011
|
+
"Member policy read should be allowed with state:read"
|
|
4012
|
+
);
|
|
3814
4013
|
}
|
|
3815
4014
|
|
|
3816
4015
|
#[test]
|
|
@@ -4129,14 +4328,8 @@ mod tests {
|
|
|
4129
4328
|
}
|
|
4130
4329
|
|
|
4131
4330
|
let bob_read = begin_read(&db, &schema, "app", bob_context).unwrap();
|
|
4132
|
-
let bob_result = execute_query(
|
|
4133
|
-
|
|
4134
|
-
&schema,
|
|
4135
|
-
&bob_read,
|
|
4136
|
-
&query,
|
|
4137
|
-
Some(PolicySubject::Owner),
|
|
4138
|
-
)
|
|
4139
|
-
.expect("query should succeed");
|
|
4331
|
+
let bob_result = execute_query(&db, &schema, &bob_read, &query, Some(PolicySubject::Owner))
|
|
4332
|
+
.expect("query should succeed");
|
|
4140
4333
|
|
|
4141
4334
|
assert_eq!(
|
|
4142
4335
|
bob_result.records.len(),
|