From: Shannon Sterz <s.sterz@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH yew-comp v2 2/3] acl: add a view and semi-generic `EditWindow` for acl entries
Date: Fri, 11 Apr 2025 15:44:32 +0200 [thread overview]
Message-ID: <20250411134435.269524-9-s.sterz@proxmox.com> (raw)
In-Reply-To: <20250411134435.269524-1-s.sterz@proxmox.com>
since each product will always have different acl paths, editing them
cannot be made completelly generic. however, this does try to make
displaying them generic based on the common api implementations in
`proxmox-access-control`.
furthermore, a the `AclEditWindow` trait makes it possible for each
product to adapt the editing panels and the number of them that are
displayed in the ACL View configurable. allowing for greater
flexibility while trying to reduce duplicate code accross multiple
products.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/acl/acl_edit.rs | 92 +++++++++++++++
src/acl/acl_view.rs | 270 ++++++++++++++++++++++++++++++++++++++++++++
src/acl/mod.rs | 5 +
src/lib.rs | 3 +
4 files changed, 370 insertions(+)
create mode 100644 src/acl/acl_edit.rs
create mode 100644 src/acl/acl_view.rs
create mode 100644 src/acl/mod.rs
diff --git a/src/acl/acl_edit.rs b/src/acl/acl_edit.rs
new file mode 100644
index 0000000..b8bbd89
--- /dev/null
+++ b/src/acl/acl_edit.rs
@@ -0,0 +1,92 @@
+use yew::html::IntoPropValue;
+
+use pwt::prelude::*;
+use pwt::widget::form::{Checkbox, FormContext};
+use pwt::widget::{FieldLabel, InputPanel};
+
+use pwt_macros::builder;
+
+use crate::EditWindow;
+use crate::{AuthidSelector, RoleSelector};
+
+pub trait AclEditWindow: Into<EditWindow> {}
+
+#[derive(Clone, PartialEq, Properties)]
+#[builder]
+pub struct AclEdit {
+ /// Use API Tokens instead of Users.
+ #[prop_or_default]
+ #[builder]
+ use_tokens: bool,
+
+ /// The endpoint which will be used to create new ACL entries via a PUT request.
+ #[prop_or(String::from("/access/acl"))]
+ #[builder(IntoPropValue, into_prop_value)]
+ acl_api_endpoint: String,
+
+ #[prop_or_default]
+ input_panel: InputPanel,
+}
+
+impl AclEdit {
+ /// Create a new `AclEdit` that takes as input a field and its label which are used to select
+ /// the ACL path for a new ACL entry.
+ pub fn new(
+ path_selector_label: impl Into<FieldLabel>,
+ path_selector: impl FieldBuilder,
+ ) -> Self {
+ let path_selector = path_selector.name("path").required(true);
+ let input_panel = InputPanel::new().with_field(path_selector_label, path_selector);
+ yew::props!(Self { input_panel })
+ }
+}
+
+impl From<AclEdit> for EditWindow {
+ fn from(value: AclEdit) -> Self {
+ let field = AuthidSelector::new().name("auth-id").required(true);
+
+ let (title, authid_label, authid_field) = if value.use_tokens {
+ (
+ tr!("API Token Permission"),
+ tr!("API Token"),
+ field.include_users(false),
+ )
+ } else {
+ (
+ tr!("User Permission"),
+ tr!("User"),
+ field.include_tokens(false),
+ )
+ };
+
+ let input_panel = value
+ .input_panel
+ .clone()
+ .padding(4)
+ .with_field(authid_label, authid_field)
+ .with_field(tr!("Role"), RoleSelector::new().name("role").required(true))
+ .with_field(
+ tr!("Propagate"),
+ Checkbox::new().name("propagate").required(true),
+ );
+
+ let url = value.acl_api_endpoint.to_owned();
+
+ let on_submit = {
+ let url = url.clone();
+ move |form_ctx: FormContext| {
+ let url = url.clone();
+ async move {
+ let data = form_ctx.get_submit_data();
+ crate::http_put(url.as_str(), Some(data)).await
+ }
+ }
+ };
+
+ EditWindow::new(title)
+ .renderer(move |_form_ctx: &FormContext| input_panel.clone().into())
+ .on_submit(on_submit)
+ }
+}
+
+impl AclEditWindow for AclEdit {}
diff --git a/src/acl/acl_view.rs b/src/acl/acl_view.rs
new file mode 100644
index 0000000..58da3fd
--- /dev/null
+++ b/src/acl/acl_view.rs
@@ -0,0 +1,270 @@
+use std::borrow::BorrowMut;
+use std::future::Future;
+use std::pin::Pin;
+use std::rc::Rc;
+
+use anyhow::Error;
+use indexmap::IndexMap;
+use serde_json::json;
+
+use yew::html::IntoPropValue;
+use yew::virtual_dom::{Key, VComp, VNode};
+
+use pwt::css;
+use pwt::prelude::*;
+use pwt::state::{Selection, Store};
+use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
+use pwt::widget::menu::{Menu, MenuButton, MenuItem};
+use pwt::widget::Toolbar;
+
+use pwt_macros::builder;
+
+use proxmox_access_control::types::{AclListItem, AclUgidType};
+
+use crate::percent_encoding::percent_encode_component;
+use crate::utils::render_boolean;
+use crate::{
+ ConfirmButton, EditWindow, LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
+};
+
+use super::acl_edit::AclEditWindow;
+
+#[derive(PartialEq, Properties)]
+#[builder]
+pub struct AclView {
+ /// Show the ACL entries for the specified API path and sub-paths only.
+ #[builder(IntoPropValue, into_prop_value)]
+ #[prop_or_default]
+ acl_path: Option<AttrValue>,
+
+ /// Specifies the endpoint from which to fetch the ACL entries from via GET and to update them
+ /// via PUT requests.
+ #[builder(IntoPropValue, into_prop_value)]
+ #[prop_or(String::from("/access/acl"))]
+ acl_api_endpoint: String,
+
+ /// Menu entries for editing the ACL. The key is used as the menu label while the value should
+ /// be a tuple containing icon class and the dialog for editing ACL entries.
+ #[prop_or_default]
+ // using an index map here preserves the insertion order
+ edit_acl_menu: IndexMap<AttrValue, (Classes, EditWindow)>,
+}
+
+impl AclView {
+ pub fn new() -> Self {
+ yew::props!(Self {})
+ }
+
+ pub fn with_acl_edit_menu_entry(
+ mut self,
+ lable: impl Into<AttrValue>,
+ icon: impl Into<Classes>,
+ dialog: impl AclEditWindow,
+ ) -> Self {
+ self.add_acl_edit_menu_entry(lable, icon, dialog);
+ self
+ }
+
+ pub fn add_acl_edit_menu_entry(
+ &mut self,
+ lable: impl Into<AttrValue>,
+ icon: impl Into<Classes>,
+ dialog: impl AclEditWindow,
+ ) {
+ self.edit_acl_menu
+ .borrow_mut()
+ .insert(lable.into(), (icon.into(), dialog.into()));
+ }
+}
+
+impl Default for AclView {
+ fn default() -> Self {
+ AclView::new()
+ }
+}
+
+impl From<AclView> for VNode {
+ fn from(value: AclView) -> Self {
+ VComp::new::<LoadableComponentMaster<ProxmoxAclView>>(Rc::new(value), None).into()
+ }
+}
+
+#[derive(Clone, PartialEq)]
+enum ViewState {
+ AddAcl(AttrValue),
+}
+
+enum Msg {
+ Reload,
+ Remove,
+}
+
+struct ProxmoxAclView {
+ selection: Selection,
+ store: Store<AclListItem>,
+}
+
+impl ProxmoxAclView {
+ fn colmuns() -> Rc<Vec<DataTableHeader<AclListItem>>> {
+ Rc::new(vec![
+ DataTableColumn::new(tr!("Path"))
+ .flex(1)
+ .render(|item: &AclListItem| item.path.as_str().into())
+ .sorter(|a: &AclListItem, b: &AclListItem| a.path.cmp(&b.path))
+ .sort_order(true)
+ .into(),
+ DataTableColumn::new(tr!("User/Group/API Token"))
+ .flex(1)
+ .render(|item: &AclListItem| item.ugid.as_str().into())
+ .sorter(|a: &AclListItem, b: &AclListItem| a.ugid.cmp(&b.ugid))
+ .sort_order(true)
+ .into(),
+ DataTableColumn::new(tr!("Role"))
+ .flex(1)
+ .render(|item: &AclListItem| item.roleid.as_str().into())
+ .sorter(|a: &AclListItem, b: &AclListItem| a.roleid.cmp(&b.roleid))
+ .into(),
+ DataTableColumn::new(tr!("Propagate"))
+ .render(|item: &AclListItem| render_boolean(item.propagate).as_str().into())
+ .sorter(|a: &AclListItem, b: &AclListItem| a.propagate.cmp(&b.propagate))
+ .into(),
+ ])
+ }
+}
+
+impl LoadableComponent for ProxmoxAclView {
+ type Properties = AclView;
+ type Message = Msg;
+ type ViewState = ViewState;
+
+ fn create(ctx: &LoadableComponentContext<Self>) -> Self {
+ let link = ctx.link();
+ link.repeated_load(5000);
+
+ let selection = Selection::new().on_select(link.callback(|_| Msg::Reload));
+
+ let store = Store::with_extract_key(|record: &AclListItem| {
+ let acl_id = format!("{} for {} - {}", record.path, record.ugid, record.roleid);
+ Key::from(acl_id)
+ });
+
+ Self { selection, store }
+ }
+
+ fn load(
+ &self,
+ ctx: &LoadableComponentContext<Self>,
+ ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
+ let store = self.store.clone();
+ let url = &ctx.props().acl_api_endpoint;
+
+ let path = if let Some(path) = &ctx.props().acl_path {
+ format!("{url}&path={}", percent_encode_component(path))
+ } else {
+ url.to_owned()
+ };
+
+ Box::pin(async move {
+ let data = crate::http_get(&path, None).await?;
+ store.write().set_data(data);
+ Ok(())
+ })
+ }
+
+ fn toolbar(&self, ctx: &LoadableComponentContext<Self>) -> Option<Html> {
+ let selected_id = self.selection.selected_key().map(|k| k.to_string());
+ let disabled = selected_id.is_none();
+
+ let mut toolbar = Toolbar::new()
+ .class("pwt-w-100")
+ .class("pwt-overflow-hidden")
+ .border_bottom(true);
+
+ if !ctx.props().edit_acl_menu.is_empty() {
+ let add_menu = ctx.props().edit_acl_menu.iter().fold(
+ Menu::new(),
+ |add_menu, (label, (icon, _))| {
+ let msg = label.to_owned();
+
+ add_menu.with_item(
+ MenuItem::new(label.to_owned())
+ .icon_class(icon.to_owned())
+ .on_select(ctx.link().change_view_callback(move |_| {
+ Some(ViewState::AddAcl(msg.clone()))
+ })),
+ )
+ },
+ );
+
+ toolbar.add_child(MenuButton::new(tr!("Add")).show_arrow(true).menu(add_menu));
+ }
+
+ toolbar.add_child(
+ ConfirmButton::new(tr!("Remove ACL Entry"))
+ .confirm_message(tr!("Are you sure you want to remove this ACL entry?"))
+ .disabled(disabled)
+ .on_activate(ctx.link().callback(|_| Msg::Remove)),
+ );
+
+ Some(toolbar.into())
+ }
+
+ fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+ match msg {
+ Msg::Reload => true,
+ Msg::Remove => {
+ if let Some(key) = self.selection.selected_key() {
+ if let Some(record) = self.store.read().lookup_record(&key).cloned() {
+ let link = ctx.link();
+ let url = ctx.props().acl_api_endpoint.to_owned();
+
+ link.clone().spawn(async move {
+ let data = match record.ugid_type {
+ AclUgidType::User => json!({
+ "delete": true,
+ "path": record.path,
+ "role": record.roleid,
+ "auth-id": record.ugid,
+ }),
+ AclUgidType::Group => json!({
+ "delete": true,
+ "path": record.path,
+ "role": record.roleid,
+ "group": record.ugid,
+ }),
+ };
+
+ match crate::http_put(url, Some(data)).await {
+ Ok(()) => link.send_reload(),
+ Err(err) => link.show_error("Removing ACL failed", err, true),
+ }
+ });
+ }
+ }
+ false
+ }
+ }
+ }
+
+ fn main_view(&self, _ctx: &LoadableComponentContext<Self>) -> Html {
+ DataTable::new(Self::colmuns(), self.store.clone())
+ .class(css::FlexFit)
+ .selection(self.selection.clone())
+ .into()
+ }
+
+ fn dialog_view(
+ &self,
+ ctx: &LoadableComponentContext<Self>,
+ view_state: &Self::ViewState,
+ ) -> Option<Html> {
+ match view_state {
+ ViewState::AddAcl(key) => ctx.props().edit_acl_menu.get(key).map(|(_, dialog)| {
+ dialog
+ .clone()
+ .on_done(ctx.link().change_view_callback(|_| None))
+ .into()
+ }),
+ }
+ }
+}
diff --git a/src/acl/mod.rs b/src/acl/mod.rs
new file mode 100644
index 0000000..cfe6aa6
--- /dev/null
+++ b/src/acl/mod.rs
@@ -0,0 +1,5 @@
+pub(crate) mod acl_edit;
+pub use acl_edit::AclEdit;
+
+pub(crate) mod acl_view;
+pub use acl_view::AclView;
diff --git a/src/lib.rs b/src/lib.rs
index 091cb72..3dacc20 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -25,6 +25,9 @@ pub use auth_edit_ldap::{AuthEditLDAP, ProxmoxAuthEditLDAP};
mod authid_selector;
pub use authid_selector::AuthidSelector;
+mod acl;
+pub use acl::{AclEdit, AclView};
+
mod bandwidth_selector;
pub use bandwidth_selector::{BandwidthSelector, ProxmoxBandwidthSelector};
--
2.39.5
_______________________________________________
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-04-11 13:45 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 1/6] access-control: add more types to prepare for api feature Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 2/6] access-control: add acl " Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 3/6] access-control: add comments to roles function of AccessControlConfig Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 4/6] access-control: add generic roles endpoint to `api` feature Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 5/6] access-control: api: refactor validation checks to re-use existing code Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 6/6] access-control: api: refactor extract_acl_node_data to be non-recursive Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH yew-comp v2 1/3] api-types/role_selector: depend on common `RoleInfo` type Shannon Sterz
2025-04-11 13:44 ` Shannon Sterz [this message]
2025-04-11 13:44 ` [pdm-devel] [PATCH yew-comp v2 3/3] role_selector/acl_edit: make api endpoint and default role configurable Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH datacenter-manager v2 1/2] server: use proxmox-access-control api implementations Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH datacenter-manager v2 2/2] ui: configuration: add panel for viewing and editing acl entries Shannon Sterz
2025-04-17 15:46 ` [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Thomas Lamprecht
2025-04-22 8:12 ` Shannon Sterz
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=20250411134435.269524-9-s.sterz@proxmox.com \
--to=s.sterz@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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal