Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
{"role": "developer", "allowed": true}
],
"session_roles": [
{"role": null, "expected_metadata": 403, "expected_connection": 403, "expected_job": 403, "expected_sql": 403},
{"role": "viewer", "expected_metadata": 200, "expected_connection": 403, "expected_job": 403, "expected_sql": 403},
{"role": "operator", "expected_metadata": 200, "expected_connection": 403, "expected_job": 200, "expected_sql": 403},
{"role": "developer", "expected_metadata": 200, "expected_connection": 200, "expected_job": 200, "expected_sql": 200},
{"role": "admin", "expected_metadata": 200, "expected_connection": 200, "expected_job": 200, "expected_sql": 200}
{"role": null, "expected_metadata": 403, "expected_connection": 403, "expected_job": 403, "expected_sql": 403, "expected_prioritised": 403},
{"role": "viewer", "expected_metadata": 200, "expected_connection": 403, "expected_job": 403, "expected_sql": 403, "expected_prioritised": 403},
{"role": "operator", "expected_metadata": 200, "expected_connection": 403, "expected_job": 200, "expected_sql": 403, "expected_prioritised": 403},
{"role": "developer", "expected_metadata": 200, "expected_connection": 200, "expected_job": 200, "expected_sql": 200, "expected_prioritised": 403},
{"role": "admin", "expected_metadata": 200, "expected_connection": 200, "expected_job": 200, "expected_sql": 200, "expected_prioritised": 200}
],
"job_settings": [
{"full_refresh": false},
Expand Down
2 changes: 1 addition & 1 deletion clients/typescript/src/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -954,7 +954,7 @@ export interface components {
* @description Closed permission vocabulary; unknown actions fail deserialization.
* @enum {string}
*/
Action: "view" | "view.sql" | "view.evidence" | "job.run" | "run.cancel" | "run.rerun" | "run.rewrite_history" | "trigger.toggle" | "sensor.reset_cursor" | "run.adhoc" | "connection.test" | "user.manage" | "role.assign" | "audit.view";
Action: "view" | "view.sql" | "view.evidence" | "job.run" | "run.cancel" | "run.rerun" | "run.rewrite_history" | "run.prioritise" | "trigger.toggle" | "sensor.reset_cursor" | "run.adhoc" | "connection.test" | "user.manage" | "role.assign" | "audit.view";
/**
* @description Administrator membership and person eligibility share the final-admin invariant.
* @enum {string}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ use crate::store::main::query::query;
use crate::store::models::{ControlTransaction, Parameter};
use crate::store::types::Row;

const ACTIONS: [Action; 14] = [
const ACTIONS: [Action; 15] = [
Action::View,
Action::ViewSql,
Action::ViewEvidence,
Action::JobRun,
Action::RunCancel,
Action::RunRerun,
Action::RunRewriteHistory,
Action::RunPrioritise,
Action::TriggerToggle,
Action::SensorResetCursor,
Action::RunAdhoc,
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlbuild-orchestration/src/credentials/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ pub enum Action {
RunRerun,
#[serde(rename = "run.rewrite_history")]
RunRewriteHistory,
#[serde(rename = "run.prioritise")]
RunPrioritise,
#[serde(rename = "trigger.toggle")]
TriggerToggle,
#[serde(rename = "sensor.reset_cursor")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,15 @@ pub(crate) async fn authorize_execution(
)
.await?;
}
if matches!(action, Action::JobRun | Action::RunRerun) && request.priority > 0 {
scope.action = Action::RunPrioritise;
(transaction, context) = authorize_person(
transaction,
(&context, credentials),
&scope,
DefaultRole::None,
)
.await?;
}
Ok((transaction, context))
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ pub(crate) struct SessionRoleCase {
pub(crate) expected_metadata: u16,
pub(crate) expected_connection: u16,
pub(crate) expected_job: u16,
/// A job run above priority 0 needs `run.prioritise`, which only Admin grants.
pub(crate) expected_prioritised: u16,
pub(crate) expected_sql: u16,
}
26 changes: 25 additions & 1 deletion crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_
let mut last_request = Value::Null;
let mut listed_person_runs = 0;
let mut launched_person_runs = 0;
let mut prioritised_runs: Vec<Value> = Vec::new();
let mut listed_seed_runs = 0;
for role_case in roles {
for role in role_case.role.iter().copied() {
Expand Down Expand Up @@ -260,7 +261,10 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_
("parent_run_id", Value::Null),
("trigger_name", Value::Null),
("rerun_of_run_id", Value::Null),
("priority", json!(0)),
(
"priority",
json!(i64::from(prioritised_runs.contains(&launched["run_id"]))),
),
("rewrite_history", json!(false)),
("cancel_requested_at", Value::Null),
] {
Expand Down Expand Up @@ -421,6 +425,24 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_
role_case.expected_job
);
launched_person_runs += usize::from(role_case.expected_job == 200);
let urgent = json!({"run_id":Uuid::now_v7(), "location_id":location, "target":"development", "job_key":"orders", "reason":"Refresh orders first", "priority":1});
assert_eq!(
request(
&provider.client,
session,
(Method::POST, "/api/control/runs"),
(&urgent, &session.csrf)
)
.await
.status()
.as_u16(),
role_case.expected_prioritised,
"a priority above 0 needs run.prioritise"
);
let accepted = usize::from(role_case.expected_prioritised == 200);
launched_person_runs += accepted;
prioritised_runs
.extend(std::iter::repeat_n(urgent["run_id"].clone(), accepted));
for (endpoint, request_body) in
[(&path[..], &body), ("/api/control/runs", &job)]
{
Expand Down Expand Up @@ -466,6 +488,8 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_
}
expected_work += usize::from(role_case.expected_connection == 200) * sessions.len();
expected_runs += usize::from(role_case.expected_job == 200) * sessions.len();
expected_runs +=
usize::from(role_case.expected_prioritised == 200) * sessions.len();
assert_eq!(
queue_counts(store).await,
json!({"work":expected_work,"runs":expected_runs,"claims":0})
Expand Down
5 changes: 3 additions & 2 deletions crates/sqlbuild-orchestration/tests/runs/test_permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,12 @@ fn given_scoped_person_roles_when_jobs_request_sensitive_settings_then_only_admi
&JobRunRequest {
run_id: source_id,
job_key: job_key.into(),
priority: 1,
..request.clone()
},
)
.await
.expect("owner creates source with frozen settings");
.expect("owner creates urgent source with frozen settings");
let cancellation = CancelRunRequest {
run_id: source_id,
event_id: Uuid::now_v7(),
Expand All @@ -226,7 +227,7 @@ fn given_scoped_person_roles_when_jobs_request_sensitive_settings_then_only_admi
.await
.ok(),
can_cancel.then_some(expected),
"cancellation permits stopping even sensitive work"
"cancellation permits stopping even sensitive or prioritised work"
);
}
let source_id = Uuid::now_v7();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ permissions = ["view.evidence"]
| Selectors | Same grammar as a job's `select`; an unknown tag or name fails publication |
| No matching nodes | Publication succeeds with a warning; the grant gives nothing |
| Permissions | Any non-administrative action; `user.manage`, `role.assign` and `audit.view` are rejected |
| Runs | `job.run`, `run.adhoc`, `run.rerun` and `run.rewrite_history` need the permission on every node the run writes |
| Runs | `job.run`, `run.adhoc`, `run.rerun` and `run.rewrite_history` need the permission on every node the run writes; a priority above 0 also needs `run.prioritise`, which only Admin grants among built-in roles |
| Triggers | `trigger.toggle` and `sensor.reset_cursor` need the permission on every node the trigger's job writes |
| Listings | The catalog, statistics and run list need `view` on every node of the current publication; partial coverage is denied, not filtered |
| Several roles | Grants add up across a person's roles |
Expand Down
1 change: 1 addition & 0 deletions ui/src/lib/api/mock/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const PLAIN: Record<Capability, string> = {
'run.cancel': 'cancel runs',
'run.rerun': 'rerun runs',
'run.rewrite_history': 'full refresh or reprocess past data',
'run.prioritise': 'run ahead of the queue',
'trigger.toggle': 'pause and resume schedules and sensors',
'sensor.reset_cursor': 'reset where a sensor is up to',
'run.adhoc': 'build any selection of nodes',
Expand Down
1 change: 1 addition & 0 deletions ui/src/lib/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const CAPABILITIES = [
'run.cancel',
'run.rerun',
'run.rewrite_history',
'run.prioritise',
'trigger.toggle',
'sensor.reset_cursor',
'run.adhoc',
Expand Down
1 change: 1 addition & 0 deletions ui/src/lib/platform/permission-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const PERMISSIONS: Record<Capability, PermissionInfo> = {
description: 'Rebuild tables from scratch or reprocess earlier periods, replacing data already built.',
risk: 'changes_data'
},
'run.prioritise': { label: 'Put runs ahead of the queue', description: 'Start or rerun a run with a priority above 0, so it is claimed before waiting runs.', risk: 'operate' },
'trigger.toggle': { label: 'Pause and resume schedules and sensors', description: 'Stop or restart automatic runs.', risk: 'operate' },
'sensor.reset_cursor': { label: 'Reset where a sensor is up to', description: 'Move a sensor’s position so it looks at events again.', risk: 'operate' },
'run.adhoc': { label: 'Build any selection of nodes', description: 'Start an ad-hoc run of nodes outside a declared job, writing to the warehouse.', risk: 'changes_data' },
Expand Down
2 changes: 1 addition & 1 deletion website/src/content/docs/docs/cli/server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ permissions = ["view.evidence"]
| Selectors | Same grammar as a job's `select`; an unknown tag or name fails publication |
| No matching nodes | Publication succeeds with a warning; the grant gives nothing |
| Permissions | Any non-administrative action; `user.manage`, `role.assign` and `audit.view` are rejected |
| Runs | `job.run`, `run.adhoc`, `run.rerun` and `run.rewrite_history` need the permission on every node the run writes |
| Runs | `job.run`, `run.adhoc`, `run.rerun` and `run.rewrite_history` need the permission on every node the run writes; a priority above 0 also needs `run.prioritise`, which only Admin grants among built-in roles |
| Triggers | `trigger.toggle` and `sensor.reset_cursor` need the permission on every node the trigger's job writes |
| Listings | The catalog, statistics and run list need `view` on every node of the current publication; partial coverage is denied, not filtered |
| Several roles | Grants add up across a person's roles |
Expand Down
Loading