Skip to content

Commit d33d83f

Browse files
committed
recombine oxql_query and oxql_query_project with scope concept
1 parent 4d4406f commit d33d83f

File tree

6 files changed

+61
-91
lines changed

6 files changed

+61
-91
lines changed

nexus/src/app/metrics.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use nexus_db_queries::{
1414
use nexus_external_api::TimeseriesSchemaPaginationParams;
1515
use nexus_types::external_api::params::SystemMetricName;
1616
use omicron_common::api::external::{Error, InternalContext};
17-
use oximeter_db::{Measurement, TimeseriesSchema};
17+
use oximeter_db::{Measurement, QueryAuthzScope, TimeseriesSchema};
1818
use std::num::NonZeroU32;
1919

2020
impl super::Nexus {
@@ -138,7 +138,7 @@ impl super::Nexus {
138138
// resources they have access to.
139139
opctx.authorize(authz::Action::Read, &authz::FLEET).await?;
140140
self.timeseries_client
141-
.oxql_query(query)
141+
.oxql_query(query, QueryAuthzScope::Fleet)
142142
.await
143143
// TODO-observability: The query method returns information
144144
// about the duration of the OxQL query and the database
@@ -162,7 +162,13 @@ impl super::Nexus {
162162
let (authz_silo, authz_project) =
163163
project_lookup.lookup_for(authz::Action::Read).await?;
164164
self.timeseries_client
165-
.oxql_query_project(query, authz_silo.id(), authz_project.id())
165+
.oxql_query(
166+
query,
167+
QueryAuthzScope::Project {
168+
silo_id: authz_silo.id(),
169+
project_id: authz_project.id(),
170+
},
171+
)
166172
.await
167173
.map(|result| result.tables)
168174
.map_err(map_timeseries_err)

oximeter/db/src/client/oxql.rs

Lines changed: 11 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::oxql::ast::table_ops::filter;
2020
use crate::oxql::ast::table_ops::filter::Filter;
2121
use crate::oxql::ast::table_ops::limit::Limit;
2222
use crate::oxql::ast::table_ops::limit::LimitKind;
23-
use crate::oxql::query::uuid_eq_filter;
23+
use crate::oxql::query::QueryAuthzScope;
2424
use crate::query::field_table_name;
2525
use oximeter::Measurement;
2626
use oximeter::TimeseriesSchema;
@@ -148,72 +148,11 @@ impl Client {
148148
pub async fn oxql_query(
149149
&self,
150150
query: impl AsRef<str>,
151+
scope: QueryAuthzScope,
151152
) -> Result<OxqlResult, Error> {
152-
// TODO-security: Need a way to implement authz checks for things like
153-
// viewing resources in another project or silo.
154-
//
155-
// I think one way to do that is look at the predicates and make sure
156-
// they refer to things the user has access to. Another is to add some
157-
// implicit predicates here, indicating the subset of fields that the
158-
// query should be able to access.
159-
//
160-
// This probably means we'll need to parse the query in Nexus, so that
161-
// we can attach the other filters ourselves.
162-
//
163-
// See https://github.com/oxidecomputer/omicron/issues/5298.
164153
let query = query.as_ref();
165154
let parsed_query = oxql::Query::new(query)?;
166-
let plan = self.build_query_plan(&parsed_query).await?;
167-
if plan.requires_full_table_scan() {
168-
return Err(Error::Oxql(anyhow::anyhow!(
169-
"This query requires at least one full table scan. \
170-
Please rewrite the query to filter either the fields \
171-
or timestamps, in order to reduce the amount of data \
172-
fetched from the database."
173-
)));
174-
}
175-
let query_id = Uuid::new_v4();
176-
let query_log =
177-
self.log.new(slog::o!("query_id" => query_id.to_string()));
178-
debug!(
179-
query_log,
180-
"parsed OxQL query";
181-
"query" => query,
182-
"parsed_query" => ?parsed_query,
183-
);
184-
let id = usdt::UniqueId::new();
185-
probes::oxql__query__start!(|| (&id, &query_id, query));
186-
let mut total_rows_fetched = 0;
187-
let result = self
188-
.run_oxql_query(
189-
&query_log,
190-
&mut self.claim_connection().await?,
191-
query_id,
192-
parsed_query,
193-
&mut total_rows_fetched,
194-
None,
195-
None,
196-
)
197-
.await;
198-
probes::oxql__query__done!(|| (&id, &query_id));
199-
result
200-
}
201-
202-
/// Run a OxQL query.
203-
pub async fn oxql_query_project(
204-
&self,
205-
query: impl AsRef<str>,
206-
silo_id: Uuid,
207-
project_id: Uuid,
208-
) -> Result<OxqlResult, Error> {
209-
let query = query.as_ref();
210-
211-
let parsed_query = oxql::Query::new(query)?;
212-
// this has to be used for everything
213-
let filtered_query = parsed_query.insert_filters(vec![
214-
uuid_eq_filter("silo_id", silo_id),
215-
uuid_eq_filter("project_id", project_id),
216-
]);
155+
let filtered_query = parsed_query.insert_authz_filters(scope);
217156

218157
let plan = self.build_query_plan(&filtered_query).await?;
219158
if plan.requires_full_table_scan() {
@@ -231,8 +170,8 @@ impl Client {
231170
query_log,
232171
"parsed OxQL query";
233172
"query" => query,
234-
"filtered_query" => ?filtered_query,
235173
"parsed_query" => ?parsed_query,
174+
"filtered_query" => ?filtered_query,
236175
);
237176
let id = usdt::UniqueId::new();
238177
probes::oxql__query__start!(|| (&id, &query_id, query));
@@ -1241,7 +1180,9 @@ fn update_total_rows_and_check(
12411180
#[cfg(test)]
12421181
mod tests {
12431182
use super::ConsistentKeyGroup;
1244-
use crate::client::oxql::chunk_consistent_key_groups_impl;
1183+
use crate::client::oxql::{
1184+
QueryAuthzScope, chunk_consistent_key_groups_impl,
1185+
};
12451186
use crate::oxql::ast::grammar::query_parser;
12461187
use crate::{Client, DATABASE_TIMESTAMP_FORMAT, DbWrite};
12471188
use crate::{Metric, Target};
@@ -1402,7 +1343,7 @@ mod tests {
14021343
"get some_target:some_metric | filter timestamp > @2020-01-01";
14031344
let result = ctx
14041345
.client
1405-
.oxql_query(query)
1346+
.oxql_query(query, QueryAuthzScope::Fleet)
14061347
.await
14071348
.expect("failed to run OxQL query");
14081349
assert_eq!(result.tables.len(), 1, "Should be exactly 1 table");
@@ -1461,7 +1402,7 @@ mod tests {
14611402
);
14621403
let result = ctx
14631404
.client
1464-
.oxql_query(&query)
1405+
.oxql_query(&query, QueryAuthzScope::Fleet)
14651406
.await
14661407
.expect("failed to run OxQL query");
14671408
assert_eq!(result.tables.len(), 1, "Should be exactly 1 table");
@@ -1517,7 +1458,7 @@ mod tests {
15171458
);
15181459
let result = ctx
15191460
.client
1520-
.oxql_query(&query)
1461+
.oxql_query(&query, QueryAuthzScope::Fleet)
15211462
.await
15221463
.expect("failed to run OxQL query");
15231464
assert_eq!(result.tables.len(), 1, "Should be exactly 1 table");
@@ -1749,7 +1690,7 @@ mod tests {
17491690
);
17501691
let result = ctx
17511692
.client
1752-
.oxql_query(&query)
1693+
.oxql_query(&query, QueryAuthzScope::Fleet)
17531694
.await
17541695
.expect("failed to run OxQL query");
17551696
assert_eq!(result.tables.len(), 1, "Should be exactly 1 table");

oximeter/db/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pub use client::TestDbWrite;
5454
pub use client::oxql::OxqlResult;
5555
pub use client::query_summary::QuerySummary;
5656
pub use model::OXIMETER_VERSION;
57+
pub use oxql::query::QueryAuthzScope;
5758

5859
#[derive(Debug, Error)]
5960
pub enum Error {

oximeter/db/src/oxql/query/mod.rs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ pub struct Query {
3737
pub(super) end_time: DateTime<Utc>,
3838
}
3939

40+
pub enum QueryAuthzScope {
41+
Fleet,
42+
Silo { silo_id: Uuid },
43+
Project { silo_id: Uuid, project_id: Uuid },
44+
}
45+
4046
impl Query {
4147
/// Construct a query written in OxQL.
4248
pub fn new(query: impl AsRef<str>) -> Result<Self, Error> {
@@ -367,19 +373,27 @@ impl Query {
367373
&self.parsed
368374
}
369375

370-
/// Insert filters after the `get`, or in the case of subqueries, recurse
371-
/// down the tree and insert them after each get.
372-
pub(crate) fn insert_filters(&self, filters: Vec<Filter>) -> Self {
373-
Self {
374-
parsed: self.parsed.insert_filters(filters),
375-
end_time: self.end_time,
376-
}
376+
/// Insert silo and project filters after the `get`, or in the case of
377+
/// subqueries, recurse down the tree and insert them after each get.
378+
pub(crate) fn insert_authz_filters(&self, scope: QueryAuthzScope) -> Self {
379+
let filtered_query = match scope {
380+
QueryAuthzScope::Fleet => self.parsed.clone(),
381+
QueryAuthzScope::Silo { silo_id } => self
382+
.parsed
383+
.insert_filters(vec![uuid_eq_filter("silo_id", silo_id)]),
384+
QueryAuthzScope::Project { silo_id, project_id } => {
385+
self.parsed.insert_filters(vec![
386+
uuid_eq_filter("silo_id", silo_id),
387+
uuid_eq_filter("project_id", project_id),
388+
])
389+
}
390+
};
391+
Self { parsed: filtered_query, end_time: self.end_time }
377392
}
378393
}
379394

380-
// TODO: move this to a more appropriate spot
381395
/// Just a helper for creating a UUID filter node concisely
382-
pub fn uuid_eq_filter(key: impl AsRef<str>, id: Uuid) -> Filter {
396+
fn uuid_eq_filter(key: impl AsRef<str>, id: Uuid) -> Filter {
383397
let simple_filter = SimpleFilter {
384398
ident: Ident(key.as_ref().to_string()),
385399
cmp: Comparison::Eq,
@@ -432,6 +446,7 @@ mod tests {
432446
use super::Filter;
433447
use super::Ident;
434448
use super::Query;
449+
use crate::QueryAuthzScope;
435450
use crate::oxql::ast::SplitQuery;
436451
use crate::oxql::ast::cmp::Comparison;
437452
use crate::oxql::ast::literal::Literal;
@@ -1041,10 +1056,8 @@ mod tests {
10411056
let query = Query::new("get a:b | filter timestamp > @now()").unwrap();
10421057
let silo_id = Uuid::new_v4();
10431058
let project_id = Uuid::new_v4();
1044-
let new_query = query.insert_filters(vec![
1045-
uuid_eq_filter("silo_id", silo_id),
1046-
uuid_eq_filter("project_id", project_id),
1047-
]);
1059+
let scope = QueryAuthzScope::Project { silo_id, project_id };
1060+
let new_query = query.insert_authz_filters(scope);
10481061

10491062
assert_eq!(query.parsed.table_ops().len(), 2);
10501063
assert_eq!(new_query.parsed.table_ops().len(), 4);
@@ -1079,7 +1092,8 @@ mod tests {
10791092
let expected_project_op =
10801093
TableOp::Basic(BasicTableOp::Filter(project_filter.clone()));
10811094

1082-
let new_query = query.insert_filters(vec![silo_filter, project_filter]);
1095+
let scope = QueryAuthzScope::Project { silo_id, project_id };
1096+
let new_query = query.insert_authz_filters(scope);
10831097

10841098
// Check top-level structure (should remain one grouped op and one filter)
10851099
let orig_ops = query.parsed.table_ops().collect::<Vec<_>>();
@@ -1131,7 +1145,8 @@ mod tests {
11311145
let expected_project_op =
11321146
TableOp::Basic(BasicTableOp::Filter(project_filter.clone()));
11331147

1134-
let new_query = query.insert_filters(vec![silo_filter, project_filter]);
1148+
let scope = QueryAuthzScope::Project { silo_id, project_id };
1149+
let new_query = query.insert_authz_filters(scope);
11351150

11361151
// Check top-level structure (should remain a single grouped op)
11371152
assert_eq!(query.parsed.table_ops().len(), 1);

oximeter/db/src/shells/oxql.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// Copyright 2024 Oxide Computer
88

99
use super::{list_timeseries, prepare_columns};
10-
use crate::{Client, OxqlResult, make_client};
10+
use crate::{Client, OxqlResult, make_client, oxql::query::QueryAuthzScope};
1111
use clap::Args;
1212
use crossterm::style::Stylize;
1313
use oxql_types::Table;
@@ -126,7 +126,10 @@ pub async fn shell(
126126
}
127127
} else {
128128
match client
129-
.oxql_query(cmd.trim().trim_end_matches(';'))
129+
.oxql_query(
130+
cmd.trim().trim_end_matches(';'),
131+
QueryAuthzScope::Fleet,
132+
)
130133
.await
131134
{
132135
Ok(result) => {

oximeter/db/tests/integration_test.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use clickward::{BasePorts, Deployment, DeploymentConfig, KeeperId};
77
use dropshot::test_util::log_prefix_for_test;
88
use omicron_test_utils::dev::poll;
99
use omicron_test_utils::dev::test_setup_log;
10+
use oximeter_db::QueryAuthzScope;
1011
use oximeter_db::{Client, DbWrite, OxqlResult, Sample, TestDbWrite};
1112
use oximeter_test_utils::wait_for_keepers;
1213
use slog::{Logger, info};
@@ -213,6 +214,7 @@ async fn test_cluster() -> anyhow::Result<()> {
213214
let oxql_res1 = client1
214215
.oxql_query(
215216
"get virtual_machine:cpu_busy | filter timestamp > @2000-01-01",
217+
QueryAuthzScope::Fleet,
216218
)
217219
.await
218220
.expect("failed to get all samples");
@@ -427,7 +429,9 @@ async fn wait_for_num_points(
427429
poll::wait_for_condition(
428430
|| async {
429431
let oxql_res = client
430-
.oxql_query("get virtual_machine:cpu_busy | filter timestamp > @2000-01-01")
432+
.oxql_query(
433+
"get virtual_machine:cpu_busy | filter timestamp > @2000-01-01",
434+
QueryAuthzScope::Fleet)
431435
.await
432436
.map_err(|_| {
433437
poll::CondCheckError::<oximeter_db::Error>::NotYet

0 commit comments

Comments
 (0)