Skip to content

Commit 81fd3a7

Browse files
committed
fix: scope trigger overrides and location listings to node coverage
trigger.toggle and sensor.reset_cursor are now authorized on every node the trigger's current job or group writes, all or nothing, as job.run is. A role covering only some of those nodes can no longer pause or reset the trigger. The catalog, statistics and run listings span the whole location, so they now need view coverage of every node in the current publication. Partial coverage is denied and names the missing nodes; results are never filtered. Validated: the native role tests (15, on SQLite and PostgreSQL), deliberate breaks of both fixes, the full orchestration native suite (106), 13 installed publication tests and make check-ci.
1 parent 234d192 commit 81fd3a7

12 files changed

Lines changed: 386 additions & 70 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//! A location-wide listing needs view coverage of every node in the current snapshot; it is never filtered.
2+
3+
use std::collections::BTreeSet;
4+
5+
use uuid::Uuid;
6+
7+
use crate::authorization::main::authorize_session::authorize_session;
8+
use crate::credentials::models::{Action, AuditContext, DefaultRole, PermissionScope};
9+
use crate::store::errors::ControlError;
10+
use crate::store::main::query::query;
11+
use crate::store::models::{ControlTransaction, Parameter};
12+
13+
pub(crate) async fn authorize_listing(
14+
transaction: ControlTransaction,
15+
credentials: (&AuditContext, (&str, &str)),
16+
(location_id, target): (Uuid, &str),
17+
default_role: DefaultRole,
18+
) -> Result<(ControlTransaction, AuditContext), ControlError> {
19+
let (transaction, rows) = query(transaction,
20+
"SELECT n.node_key FROM sqb_snapshot_nodes n JOIN sqb_current_snapshots c ON c.snapshot_id=n.snapshot_id \
21+
WHERE c.location_id=$1 AND c.target=$2",
22+
vec![Parameter::Id(location_id), Parameter::Text(target.into())]).await?;
23+
let mut nodes: BTreeSet<String> = BTreeSet::new();
24+
for row in &rows {
25+
nodes.insert(row["node_key"].as_str().unwrap_or_default().into());
26+
}
27+
let scope = PermissionScope {
28+
action: Action::View,
29+
location_id: Some(location_id),
30+
target: Some(target.into()),
31+
nodes,
32+
};
33+
authorize_session(transaction, credentials, &scope, default_role).await
34+
}

‎crates/sqlbuild-orchestration/src/authorization/main/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod assign_builtin_role;
2+
pub(crate) mod authorize_listing;
23
pub(crate) mod authorize_session;
34
pub mod bootstrap_user;
45
pub mod change_administrator;

‎crates/sqlbuild-orchestration/src/declarations/_helpers/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ pub(crate) mod documents;
33
pub(crate) mod operator_change;
44
pub(crate) mod operator_state;
55
pub(crate) mod publication;
6+
pub(crate) mod trigger_scope;
67
pub(crate) mod trigger_state;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//! A trigger override's permission scope is every node the trigger's current job or group writes.
2+
3+
use uuid::Uuid;
4+
5+
use crate::credentials::models::{Action, AuditContext, PermissionScope};
6+
use crate::declarations::models::{TriggerChange, TriggerChangeRequest};
7+
use crate::runs::main::frozen_job_request::frozen_job_request;
8+
use crate::runs::models::JobRunRequest;
9+
use crate::snapshots::main::job_write_set::job_write_set;
10+
use crate::store::errors::ControlError;
11+
use crate::store::main::query::query;
12+
use crate::store::models::{ControlTransaction, Parameter};
13+
14+
/// A group's scope is its frozen selection's write set, which covers every child's write set.
15+
pub(crate) async fn trigger_scope(
16+
transaction: ControlTransaction,
17+
context: &AuditContext,
18+
request: &TriggerChangeRequest,
19+
) -> Result<(ControlTransaction, PermissionScope), ControlError> {
20+
let (transaction, rows) = query(transaction,
21+
"SELECT location_id,target,job_key FROM sqb_triggers WHERE trigger_id=$1 AND removed_in_snapshot_id IS NULL",
22+
vec![Parameter::Id(request.trigger_id)]).await?;
23+
let row = rows
24+
.first()
25+
.ok_or_else(|| ControlError::Invalid("current trigger not found".into()))?;
26+
let job = JobRunRequest {
27+
run_id: Uuid::nil(),
28+
location_id: serde_json::from_value(row["location_id"].clone())
29+
.map_err(|error| ControlError::Invalid(error.to_string()))?,
30+
target: row["target"].as_str().unwrap_or_default().into(),
31+
job_key: row["job_key"].as_str().unwrap_or_default().into(),
32+
reason: request.reason.clone(),
33+
priority: 0,
34+
window: None,
35+
};
36+
let (transaction, frozen) = frozen_job_request(transaction, context, &job).await?;
37+
let (transaction, nodes) =
38+
job_write_set(transaction, frozen.snapshot_id, &frozen.selection).await?;
39+
let action = match request.change {
40+
TriggerChange::Enable | TriggerChange::Pause | TriggerChange::ResetDefault => {
41+
Action::TriggerToggle
42+
}
43+
TriggerChange::SetCursor { .. } | TriggerChange::ResetCursor => Action::SensorResetCursor,
44+
};
45+
Ok((
46+
transaction,
47+
PermissionScope {
48+
action,
49+
location_id: Some(job.location_id),
50+
target: Some(job.target),
51+
nodes,
52+
},
53+
))
54+
}

‎crates/sqlbuild-orchestration/src/declarations/main/change_person_trigger.rs‎

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,28 @@
1-
//! Resolve a trigger's real location before authorizing an authenticated runtime override.
1+
//! A trigger override needs its permission on every node the trigger's current job or group writes.
22
33
use serde_json::json;
44

55
use crate::authorization::main::authorize_session::authorize_session;
66
use crate::authorization::main::deny_authorization::deny;
7-
use crate::credentials::models::{Action, AuditContext, DefaultRole, PermissionScope};
7+
use crate::credentials::models::{AuditContext, DefaultRole};
88
use crate::declarations::_helpers::operator_change::change_trigger_in;
9-
use crate::declarations::models::{TriggerChange, TriggerChangeRequest};
9+
use crate::declarations::_helpers::trigger_scope::trigger_scope;
10+
use crate::declarations::models::TriggerChangeRequest;
1011
use crate::store::errors::ControlError;
11-
use crate::store::main::{begin::begin, commit::commit, query::query};
12-
use crate::store::models::{ControlStore, Parameter, TransactionMode};
12+
use crate::store::main::{begin::begin, commit::commit};
13+
use crate::store::models::{ControlStore, TransactionMode};
1314

1415
pub async fn change_person_trigger(
1516
store: &ControlStore,
1617
(context, credentials): (&AuditContext, (&str, &str)),
1718
request: &TriggerChangeRequest,
1819
) -> Result<bool, ControlError> {
19-
let (transaction, rows) = query(begin(store, TransactionMode::Write).await?,
20-
"SELECT location_id,target FROM sqb_triggers WHERE trigger_id=$1 AND removed_in_snapshot_id IS NULL",
21-
vec![Parameter::Id(request.trigger_id)]).await?;
22-
let row = rows
23-
.first()
24-
.ok_or_else(|| ControlError::Invalid("current trigger not found".into()))?;
25-
let scope = PermissionScope {
26-
action: match request.change {
27-
TriggerChange::Enable | TriggerChange::Pause | TriggerChange::ResetDefault => {
28-
Action::TriggerToggle
29-
}
30-
TriggerChange::SetCursor { .. } | TriggerChange::ResetCursor => {
31-
Action::SensorResetCursor
32-
}
33-
},
34-
location_id: serde_json::from_value(row["location_id"].clone())
35-
.map_err(|error| ControlError::Invalid(error.to_string()))?,
36-
target: serde_json::from_value(row["target"].clone())
37-
.map_err(|error| ControlError::Invalid(error.to_string()))?,
38-
nodes: Default::default(),
39-
};
20+
let (transaction, scope) = trigger_scope(
21+
begin(store, TransactionMode::Write).await?,
22+
context,
23+
request,
24+
)
25+
.await?;
4026
let (transaction, context) = authorize_session(
4127
transaction,
4228
(context, credentials),

‎crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
33
use uuid::Uuid;
44

5-
use crate::authorization::main::authorize_session::authorize_session;
6-
use crate::credentials::models::{Action, AuditContext, DefaultRole, PermissionScope};
5+
use crate::authorization::main::authorize_listing::authorize_listing;
6+
use crate::credentials::models::{AuditContext, DefaultRole};
77
use crate::runs::_helpers::lifecycle::reads::list_runs_in;
88
use crate::runs::models::RunRecord;
99
use crate::store::errors::ControlError;
@@ -16,15 +16,10 @@ pub async fn list_person_runs(
1616
(location_id, target, limit): (Uuid, &str, u32),
1717
default_role: DefaultRole,
1818
) -> Result<Vec<RunRecord>, ControlError> {
19-
let (transaction, _) = authorize_session(
19+
let (transaction, _) = authorize_listing(
2020
begin(store, TransactionMode::Write).await?,
2121
(context, credentials),
22-
&PermissionScope {
23-
action: Action::View,
24-
location_id: Some(location_id),
25-
target: Some(target.into()),
26-
nodes: Default::default(),
27-
},
22+
(location_id, target),
2823
default_role,
2924
)
3025
.await?;

‎crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs‎

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
33
use uuid::Uuid;
44

5-
use crate::credentials::models::{Action, AuditContext, DefaultRole, PermissionScope};
5+
use crate::authorization::main::authorize_listing::authorize_listing;
6+
use crate::credentials::models::{AuditContext, DefaultRole};
67
use crate::store::errors::ControlError;
7-
use crate::store::main::commit::commit;
8-
use crate::store::models::ControlStore;
8+
use crate::store::main::{begin::begin, commit::commit};
9+
use crate::store::models::{ControlStore, TransactionMode};
910
use crate::warehouse_metadata::_helpers::catalog::read_catalog_page;
10-
use crate::warehouse_metadata::_helpers::person_reads::begin_view;
1111
use crate::warehouse_metadata::models::{CatalogPage, CatalogQuery};
1212

1313
pub async fn read_person_catalog(
@@ -16,15 +16,10 @@ pub async fn read_person_catalog(
1616
(location_id, target, options): (Uuid, &str, &CatalogQuery),
1717
default_role: DefaultRole,
1818
) -> Result<CatalogPage, ControlError> {
19-
let transaction = begin_view(
20-
store,
19+
let (transaction, _) = authorize_listing(
20+
begin(store, TransactionMode::Write).await?,
2121
(context, credentials),
22-
&PermissionScope {
23-
action: Action::View,
24-
location_id: Some(location_id),
25-
target: Some(target.into()),
26-
nodes: Default::default(),
27-
},
22+
(location_id, target),
2823
default_role,
2924
)
3025
.await?;

‎crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs‎

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
33
use uuid::Uuid;
44

5-
use crate::credentials::models::{Action, AuditContext, DefaultRole, PermissionScope};
5+
use crate::authorization::main::authorize_listing::authorize_listing;
6+
use crate::credentials::models::{AuditContext, DefaultRole};
67
use crate::store::errors::ControlError;
7-
use crate::store::main::commit::commit;
8-
use crate::store::models::ControlStore;
9-
use crate::warehouse_metadata::_helpers::person_reads::begin_view;
8+
use crate::store::main::{begin::begin, commit::commit};
9+
use crate::store::models::{ControlStore, TransactionMode};
1010
use crate::warehouse_metadata::_helpers::statistics::read_statistics_page;
1111
use crate::warehouse_metadata::models::{StatisticsPage, StatisticsQuery, StatisticsRetention};
1212

@@ -16,15 +16,10 @@ pub async fn read_person_statistics(
1616
(location_id, target, options): (Uuid, &str, &StatisticsQuery),
1717
(default_role, retention): (DefaultRole, StatisticsRetention),
1818
) -> Result<StatisticsPage, ControlError> {
19-
let transaction = begin_view(
20-
store,
19+
let (transaction, _) = authorize_listing(
20+
begin(store, TransactionMode::Write).await?,
2121
(context, credentials),
22-
&PermissionScope {
23-
action: Action::View,
24-
location_id: Some(location_id),
25-
target: Some(target.into()),
26-
nodes: Default::default(),
27-
},
22+
(location_id, target),
2823
default_role,
2924
)
3025
.await?;

0 commit comments

Comments
 (0)