From: Lukas Wagner <l.wagner@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager 2/3] views: add 'include-all' param; change semantics when there are no includes
Date: Mon, 17 Nov 2025 15:11:21 +0100 [thread overview]
Message-ID: <20251117141122.328559-3-l.wagner@proxmox.com> (raw)
In-Reply-To: <20251117141122.328559-1-l.wagner@proxmox.com>
Previously, having no 'include' rules would mean that *all* resources
are included. Due view permissions being transitively applied to all
included resources, this also has some security consequences.
For instance, if there is only a single include rule which is then
removed by accident, suddenly any AuthID having privileges on the view
ACL object would then be granted these privileges on *all* resources.
To somewhat mitigate this, the semantics are slightly changed. If there
are no include rules, then no resources will be contained in the view.
A new 'include-all' parameter is added, which aims to emphasize the
intent of *really* including everything.
Suggested-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
lib/pdm-api-types/src/views.rs | 4 +++
server/src/views/mod.rs | 60 +++++++++++++++++++---------------
server/src/views/tests.rs | 35 ++++++++++++++++++--
3 files changed, 69 insertions(+), 30 deletions(-)
diff --git a/lib/pdm-api-types/src/views.rs b/lib/pdm-api-types/src/views.rs
index 950a3a04..82ab8781 100644
--- a/lib/pdm-api-types/src/views.rs
+++ b/lib/pdm-api-types/src/views.rs
@@ -58,6 +58,9 @@ pub struct ViewConfig {
#[updater(skip)]
pub id: String,
+ /// Include all resources by default.
+ pub include_all: Option<bool>,
+
/// List of includes.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[updater(serde(skip_serializing_if = "Option::is_none"))]
@@ -261,6 +264,7 @@ mod test {
fn config_smoke_test() {
let config = "
view: some-view
+ include-all true
include exact:remote=someremote
include remote=someremote
include resource-type=qemu
diff --git a/server/src/views/mod.rs b/server/src/views/mod.rs
index 8b4c70a4..3669ace4 100644
--- a/server/src/views/mod.rs
+++ b/server/src/views/mod.rs
@@ -64,31 +64,34 @@ impl View {
/// When there are `include remote:<...>` or `exclude remote:<...>` rules, we can use these to
/// check if a remote needs to be considered at all.
pub fn can_skip_remote(&self, remote: &str) -> bool {
- let mut has_any_include_remote = false;
- let mut matches_any_include_remote = false;
-
- let mut any_other = false;
-
- for include in &self.config.include {
- if let FilterRule::Remote(r) = include {
- has_any_include_remote = true;
- if r.matches(remote) {
- matches_any_include_remote = true;
- break;
- }
- } else {
- any_other = true;
- }
- }
-
let matches_any_exclude_remote = self
.config
.exclude
.iter()
.any(|rule| Self::matches_remote_rule(remote, rule));
- (has_any_include_remote && !matches_any_include_remote && !any_other)
- || matches_any_exclude_remote
+ if matches_any_exclude_remote {
+ return true;
+ }
+
+ if self.config.include_all.unwrap_or_default() {
+ return false;
+ }
+
+ for include in &self.config.include {
+ if let FilterRule::Remote(r) = include {
+ if r.matches(remote) {
+ return false;
+ }
+ } else {
+ // If there is any other type of rule, we cannot safely infer whether we can skip
+ // the remote (e.g. for 'tag' matches, we have to check *all* remotes for resources
+ // with a given tag)
+ return false;
+ }
+ }
+
+ true
}
/// Check if a remote is *explicitly* included (and not excluded).
@@ -96,18 +99,22 @@ impl View {
/// A subset of the resources of a remote might still be pulled in by other rules,
/// but this function check if the remote as a whole is matched.
pub fn is_remote_explicitly_included(&self, remote: &str) -> bool {
- let matches_include_remote = self
- .config
- .include
- .iter()
- .any(|rule| Self::matches_remote_rule(remote, rule));
+ let included = if self.config.include_all.unwrap_or_default() {
+ true
+ } else {
+ self.config
+ .include
+ .iter()
+ .any(|rule| Self::matches_remote_rule(remote, rule))
+ };
+
let matches_exclude_remote = self
.config
.exclude
.iter()
.any(|rule| Self::matches_remote_rule(remote, rule));
- matches_include_remote && !matches_exclude_remote
+ included && !matches_exclude_remote
}
/// Check if a node is matched by the filter rules.
@@ -131,8 +138,7 @@ impl View {
}
fn check_if_included(&self, remote: &str, resource: &ResourceData) -> bool {
- if self.config.include.is_empty() {
- // If there are no include rules, any resource is included (unless excluded)
+ if self.config.include_all.unwrap_or_default() {
return true;
}
diff --git a/server/src/views/tests.rs b/server/src/views/tests.rs
index 14d94ac9..0d83ae70 100644
--- a/server/src/views/tests.rs
+++ b/server/src/views/tests.rs
@@ -135,6 +135,7 @@ fn exclude_remotes() {
FilterRule::Remote(StringMatcher::Exact("remote-a".into())),
FilterRule::Remote(StringMatcher::Exact("remote-b".into())),
],
+ include_all: Some(true),
..Default::default()
};
@@ -184,6 +185,8 @@ fn include_exclude_remotes() {
FilterRule::Remote(StringMatcher::Exact("remote-b".into())),
FilterRule::Remote(StringMatcher::Exact("remote-c".into())),
],
+
+ ..Default::default()
};
run_test(
config.clone(),
@@ -224,6 +227,7 @@ fn include_exclude_remotes() {
fn empty_config() {
let config = ViewConfig {
id: "empty".into(),
+ include_all: Some(true),
..Default::default()
};
run_test(
@@ -301,6 +305,7 @@ fn exclude_type() {
FilterRule::ResourceType(EnumMatcher(ResourceType::PveStorage)),
FilterRule::ResourceType(EnumMatcher(ResourceType::PveQemu)),
],
+ include_all: Some(true),
..Default::default()
},
&[
@@ -329,6 +334,7 @@ fn include_exclude_type() {
exclude: vec![FilterRule::ResourceType(EnumMatcher(
ResourceType::PveStorage,
))],
+ ..Default::default()
},
&[
(
@@ -357,6 +363,7 @@ fn include_exclude_tags() {
FilterRule::Tag(StringMatcher::Exact("tag2".to_string())),
],
exclude: vec![FilterRule::Tag(StringMatcher::Exact("tag3".to_string()))],
+ ..Default::default()
},
&[
(
@@ -404,6 +411,7 @@ fn include_exclude_resource_pool() {
exclude: vec![FilterRule::ResourcePool(StringMatcher::Exact(
"pool2".to_string(),
))],
+ ..Default::default()
},
&[
(
@@ -459,6 +467,7 @@ fn include_exclude_resource_id() {
"remote/{REMOTE}/storage/{NODE}/otherstorage"
))),
],
+ ..Default::default()
},
&[
(
@@ -509,6 +518,7 @@ fn node_included() {
exclude: vec![FilterRule::Remote(StringMatcher::Exact(
"remote-b".to_string(),
))],
+ ..Default::default()
});
assert!(view.is_node_included("remote-a", "somenode"));
@@ -528,6 +538,7 @@ fn can_skip_remote_if_excluded() {
exclude: vec![FilterRule::Remote(StringMatcher::Exact(
"remote-b".to_string(),
))],
+ include_all: Some(true),
});
assert!(!view.can_skip_remote("remote-a"));
@@ -542,6 +553,7 @@ fn can_skip_remote_if_included() {
"remote-b".to_string(),
))],
exclude: vec![],
+ ..Default::default()
});
assert!(!view.can_skip_remote("remote-b"));
@@ -559,6 +571,7 @@ fn can_skip_remote_cannot_skip_if_any_other_include() {
)),
],
exclude: vec![],
+ ..Default::default()
});
assert!(!view.can_skip_remote("remote-b"));
@@ -575,6 +588,7 @@ fn can_skip_remote_explicit_remote_exclude() {
exclude: vec![FilterRule::Remote(StringMatcher::Exact(
"remote-a".to_string(),
))],
+ ..Default::default()
});
assert!(view.can_skip_remote("remote-a"));
@@ -584,8 +598,19 @@ fn can_skip_remote_explicit_remote_exclude() {
fn can_skip_remote_with_empty_config() {
let view = View::new(ViewConfig {
id: "abc".into(),
- include: vec![],
- exclude: vec![],
+ ..Default::default()
+ });
+
+ assert!(view.can_skip_remote("remote-a"));
+ assert!(view.can_skip_remote("remote-b"));
+}
+
+#[test]
+fn can_skip_remote_cannot_skip_if_all_included() {
+ let view = View::new(ViewConfig {
+ id: "abc".into(),
+ include_all: Some(true),
+ ..Default::default()
});
assert!(!view.can_skip_remote("remote-a"));
@@ -600,6 +625,7 @@ fn can_skip_remote_with_no_remote_includes() {
"resource/remote-a/guest/100".to_string(),
))],
exclude: vec![],
+ ..Default::default()
});
assert!(!view.can_skip_remote("remote-a"));
@@ -614,6 +640,7 @@ fn explicitly_included_remote() {
"remote-b".to_string(),
))],
exclude: vec![],
+ ..Default::default()
});
assert!(view.is_remote_explicitly_included("remote-b"));
@@ -629,6 +656,7 @@ fn included_and_excluded_same_remote() {
exclude: vec![FilterRule::Remote(StringMatcher::Exact(
"remote-b".to_string(),
))],
+ ..Default::default()
});
assert!(!view.is_remote_explicitly_included("remote-b"));
@@ -640,8 +668,9 @@ fn not_explicitly_included_remote() {
id: "abc".into(),
include: vec![],
exclude: vec![],
+ include_all: Some(true),
});
// Assert that is not *explicitly* included
- assert!(!view.is_remote_explicitly_included("remote-b"));
+ assert!(view.is_remote_explicitly_included("remote-b"));
}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
next prev parent reply other threads:[~2025-11-17 14:12 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-11-17 14:11 [pdm-devel] [PATCH datacenter-manager 0/3] views: preparations for regex/glob, include-all param Lukas Wagner
2025-11-17 14:11 ` [pdm-devel] [PATCH datacenter-manager 1/3] pdm-api-types: views: preparations for future glob/regex support Lukas Wagner
2025-11-17 14:11 ` Lukas Wagner [this message]
2025-11-17 14:11 ` [pdm-devel] [PATCH datacenter-manager 3/3] views: tests: use full section-config format for test cases Lukas Wagner
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20251117141122.328559-3-l.wagner@proxmox.com \
--to=l.wagner@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox