559 lines
21 KiB
Rust
Executable File
559 lines
21 KiB
Rust
Executable File
use std::{
|
|
borrow::Borrow,
|
|
io,
|
|
path::PathBuf,
|
|
thread::{self, JoinHandle},
|
|
};
|
|
|
|
use anyhow::Result;
|
|
use ratatui::{
|
|
crossterm::event::{Event, KeyCode, KeyModifiers},
|
|
layout::{Constraint, Flex, Layout, Rect},
|
|
style::{Color, Style},
|
|
text::{Line, Span},
|
|
widgets::{Block, Clear, Paragraph, Row, Table, TableState, Wrap},
|
|
Frame,
|
|
};
|
|
|
|
use crate::helper::getent::{get_groups, get_users, Group, User};
|
|
|
|
use posix_acl::{ACLEntry, PermSet, PosixACL, Qualifier, ACL_RWX};
|
|
|
|
use super::Component;
|
|
|
|
pub struct ACLEditor {
|
|
path: PathBuf,
|
|
is_changed: bool,
|
|
available_users: Vec<User>,
|
|
available_groups: Vec<Group>,
|
|
acl: PosixACL,
|
|
|
|
pub exited: bool,
|
|
|
|
save_thread: Option<JoinHandle<Result<()>>>,
|
|
save_thread_error: Option<String>,
|
|
save_thread_ok: Option<String>,
|
|
|
|
table_state: TableState,
|
|
|
|
user_popup: Option<SelectUserOrGroupPopup<User>>,
|
|
group_popup: Option<SelectUserOrGroupPopup<Group>>,
|
|
|
|
focused: bool,
|
|
}
|
|
|
|
impl Default for ACLEditor {
|
|
fn default() -> Self {
|
|
Self {
|
|
path: PathBuf::new(),
|
|
is_changed: false,
|
|
available_groups: get_groups().unwrap(),
|
|
available_users: get_users().unwrap(),
|
|
acl: PosixACL::new(ACL_RWX, ACL_RWX, ACL_RWX),
|
|
exited: false,
|
|
save_thread: None,
|
|
save_thread_error: None,
|
|
save_thread_ok: None,
|
|
table_state: TableState::default(),
|
|
user_popup: None,
|
|
group_popup: None,
|
|
focused: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Component for ACLEditor {
|
|
fn update(&mut self, mut event: super::AppEventClient) {
|
|
if let Some(save_thread) = &self.save_thread {
|
|
// Mark all events as handled, since this task should not be interrupted
|
|
// by any other event
|
|
event.mark_as_handled();
|
|
|
|
if save_thread.is_finished() {
|
|
let save_thread = std::mem::replace(&mut self.save_thread, None).unwrap();
|
|
let result = save_thread.join();
|
|
match result {
|
|
Ok(Ok(_)) => {
|
|
self.save_thread_ok = Some("Saved successfully!".to_string());
|
|
self.save_thread_error = None;
|
|
self.is_changed = false;
|
|
}
|
|
Ok(Err(e)) => {
|
|
self.save_thread_error = Some(format!("Failed to save {:?}", e));
|
|
}
|
|
Err(e) => {
|
|
self.save_thread_error = Some(format!("Failed to join {:?}", e));
|
|
}
|
|
}
|
|
}
|
|
} else if self.save_thread_error.is_some() {
|
|
// TODO: Might want to do something, or not
|
|
if event.is_focused_and_key_pressed(KeyCode::Char('o'), None) {
|
|
event.mark_as_handled();
|
|
self.save_thread_error = None;
|
|
}
|
|
|
|
if event.has_focus() && self.save_thread_error.is_some() {
|
|
event.register_helper("o", "Accept error");
|
|
}
|
|
} else if self.save_thread_ok.is_some() {
|
|
if event.is_focused_and_key_pressed(KeyCode::Char('o'), None) {
|
|
event.mark_as_handled();
|
|
self.save_thread_ok = None;
|
|
}
|
|
|
|
if event.has_focus() && self.save_thread_ok.is_some() {
|
|
event.register_helper("o", "Accept message");
|
|
}
|
|
} else {
|
|
if let Some(user_popup) = &mut self.user_popup {
|
|
user_popup.update(event.get_client(true));
|
|
if user_popup.exited() {
|
|
if let Some(selected) = user_popup.get_result() {
|
|
self.acl
|
|
.entries
|
|
.push(ACLEntry(Qualifier::User(selected.uid), PermSet::ACL_RWX));
|
|
self.is_changed = true;
|
|
}
|
|
self.user_popup = None;
|
|
} else {
|
|
event.has_focus = false;
|
|
}
|
|
} else if let Some(group_popup) = &mut self.group_popup {
|
|
group_popup.update(event.get_client(true));
|
|
if group_popup.exited() {
|
|
if let Some(selected) = group_popup.get_result() {
|
|
self.acl
|
|
.entries
|
|
.push(ACLEntry(Qualifier::Group(selected.gid), PermSet::ACL_RWX));
|
|
self.is_changed = true;
|
|
}
|
|
self.group_popup = None;
|
|
} else {
|
|
event.has_focus = false;
|
|
}
|
|
}
|
|
|
|
if event.is_focused_and_key_pressed(KeyCode::Esc, None) {
|
|
event.mark_as_handled();
|
|
self.exited = true;
|
|
event.has_focus = false;
|
|
} else if event.is_focused_and_key_pressed(KeyCode::Up, None) {
|
|
event.mark_as_handled();
|
|
self.table_state.select_previous();
|
|
} else if event.is_focused_and_key_pressed(KeyCode::Down, None) {
|
|
event.mark_as_handled();
|
|
self.table_state.select_next();
|
|
} else if event.is_focused_and_key_pressed(KeyCode::Char('d'), None) {
|
|
event.mark_as_handled();
|
|
if let Some(selected) = self.table_state.selected() {
|
|
let _entry = self.acl.entries.remove(selected);
|
|
self.is_changed = true;
|
|
self.table_state.select(None);
|
|
}
|
|
} else if event.is_focused_and_key_pressed(KeyCode::Char('u'), None) {
|
|
event.mark_as_handled();
|
|
let mut popup = SelectUserOrGroupPopup::new_users();
|
|
popup.update(event.get_client(true));
|
|
self.user_popup = Some(popup);
|
|
event.has_focus = false;
|
|
} else if event.is_focused_and_key_pressed(KeyCode::Char('g'), None) {
|
|
event.mark_as_handled();
|
|
let mut popup = SelectUserOrGroupPopup::new_groups();
|
|
popup.update(event.get_client(true));
|
|
self.group_popup = Some(popup);
|
|
event.has_focus = false;
|
|
} else if event.is_focused_and_key_pressed(KeyCode::Char('a'), None) {
|
|
event.mark_as_handled();
|
|
let path = self.path.clone();
|
|
let mut acl = self.acl.clone();
|
|
acl.set(ACLEntry(Qualifier::Mask, PermSet::all())); // Make sure mask is set!
|
|
|
|
self.save_thread_error = None;
|
|
self.save_thread_ok = None;
|
|
self.save_thread = Some(std::thread::spawn(move || {
|
|
// thread::sleep(std::time::Duration::from_secs());
|
|
crate::helper::acl_writer::write_acl_recursive(path, acl)
|
|
}));
|
|
}
|
|
|
|
self.acl.entries.iter_mut().enumerate().for_each(|(i, e)| {
|
|
if self.table_state.selected() == Some(i) {
|
|
event
|
|
.is_focused_and_key_pressed(KeyCode::Char('x'), None)
|
|
.then(|| {
|
|
event.mark_as_handled();
|
|
if e.1.contains(PermSet::ACL_EXECUTE) {
|
|
e.1.remove(PermSet::ACL_EXECUTE);
|
|
} else {
|
|
e.1.insert(PermSet::ACL_EXECUTE);
|
|
}
|
|
self.is_changed = true;
|
|
});
|
|
|
|
event
|
|
.is_focused_and_key_pressed(KeyCode::Char('r'), None)
|
|
.then(|| {
|
|
event.mark_as_handled();
|
|
if e.1.contains(PermSet::ACL_READ) {
|
|
e.1.remove(PermSet::ACL_READ);
|
|
} else {
|
|
e.1.insert(PermSet::ACL_READ);
|
|
}
|
|
self.is_changed = true;
|
|
});
|
|
|
|
event
|
|
.is_focused_and_key_pressed(KeyCode::Char('w'), None)
|
|
.then(|| {
|
|
event.mark_as_handled();
|
|
if e.1.contains(PermSet::ACL_WRITE) {
|
|
e.1.remove(PermSet::ACL_WRITE);
|
|
} else {
|
|
e.1.insert(PermSet::ACL_WRITE);
|
|
}
|
|
self.is_changed = true;
|
|
});
|
|
}
|
|
});
|
|
|
|
if event.has_focus() {
|
|
event.register_helper("Esc", "Close editor");
|
|
event.register_helper("↑", "Select previous entry");
|
|
event.register_helper("↓", "Select next entry");
|
|
event.register_helper("r", "Toggle Read permission");
|
|
event.register_helper("w", "Toggle Write permission");
|
|
event.register_helper("x", "Toggle Execute permission");
|
|
event.register_helper("d", "Delete selected entry");
|
|
event.register_helper("u", "Create new user entry");
|
|
event.register_helper("g", "Create new group entry");
|
|
event.register_helper("a", "Apply changes");
|
|
}
|
|
}
|
|
|
|
self.focused = event.has_focus();
|
|
}
|
|
|
|
fn render(&mut self, f: &mut Frame, rect: ratatui::prelude::Rect) {
|
|
let block = Block::bordered()
|
|
.title(format!("Editing: {:?}", self.path))
|
|
.borders(ratatui::widgets::Borders::ALL)
|
|
.border_style(ratatui::style::Style::default().fg(if self.focused {
|
|
ratatui::style::Color::Cyan
|
|
} else {
|
|
ratatui::style::Color::Gray
|
|
}));
|
|
let inner_area = block.inner(rect);
|
|
f.render_widget(block, rect);
|
|
|
|
// todo!("Implement the rendering logic for ACLEditor");
|
|
if self.save_thread.is_some() {
|
|
let lines = vec![
|
|
Line::from(Span::styled("Saving...", Style::new().fg(Color::Yellow))),
|
|
// Line::from("Press any key to cancel..."),
|
|
];
|
|
let pg = Paragraph::new(lines).wrap(Wrap::default());
|
|
f.render_widget(pg, inner_area);
|
|
} else if let Some(ok) = &self.save_thread_ok {
|
|
let lines = vec![
|
|
Line::from(Span::styled(
|
|
"Saved successfully!",
|
|
Style::new().fg(Color::Green),
|
|
)),
|
|
Line::from(ok.as_str()),
|
|
// Line::from("Press any key to continue..."),
|
|
];
|
|
let pg = Paragraph::new(lines).wrap(Wrap::default());
|
|
f.render_widget(pg, inner_area);
|
|
} else if let Some(err) = &self.save_thread_error {
|
|
let lines = vec![
|
|
Line::from(Span::styled(
|
|
"Error while saving!",
|
|
Style::new().fg(Color::Red),
|
|
)),
|
|
Line::from(err.as_str()),
|
|
// Line::from("Press any key to continue..."),
|
|
];
|
|
let pg = Paragraph::new(lines).wrap(Wrap::default());
|
|
f.render_widget(pg, inner_area);
|
|
} else {
|
|
let layout = Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]);
|
|
let rects = layout.split(inner_area);
|
|
|
|
let acl_table = Table::new(
|
|
self.acl.entries.iter_mut().map(|e| {
|
|
let mut cols = vec![];
|
|
let label = match e.0 {
|
|
Qualifier::User(_) => "U",
|
|
Qualifier::Group(_) => "G",
|
|
Qualifier::Other => "",
|
|
Qualifier::GroupObj => "G",
|
|
Qualifier::UserObj => "U",
|
|
Qualifier::Mask => "",
|
|
};
|
|
cols.push(label.to_string());
|
|
|
|
let label = match e.0 {
|
|
Qualifier::User(uid) => self
|
|
.available_users
|
|
.iter()
|
|
.find(|user| user.uid == uid)
|
|
.map(|user| user.name.clone())
|
|
.unwrap_or_else(|| {
|
|
self.available_groups
|
|
.iter()
|
|
.find(|group| group.gid == uid)
|
|
.map(|group| format!("({})", group.name))
|
|
.unwrap_or_else(|| format!("Unknown user {}", uid))
|
|
}),
|
|
Qualifier::Group(gid) => self
|
|
.available_groups
|
|
.iter()
|
|
.find(|group| group.gid == gid)
|
|
.map(|group| group.name.clone())
|
|
.unwrap_or_else(|| {
|
|
self.available_users
|
|
.iter()
|
|
.find(|user| user.uid == gid)
|
|
.map(|user| format!("({})", user.name))
|
|
.unwrap_or_else(|| format!("Unknown group {}", gid))
|
|
}),
|
|
Qualifier::Other => "Jeder".to_string(),
|
|
Qualifier::GroupObj => "Gruppe".to_string(),
|
|
Qualifier::UserObj => "Benutzer".to_string(),
|
|
Qualifier::Mask => "Mask".to_string(),
|
|
};
|
|
|
|
cols.push(label);
|
|
|
|
let r = e.1.contains(PermSet::ACL_READ);
|
|
cols.push((if r { "✔" } else { "x" }).to_string());
|
|
let w = e.1.contains(PermSet::ACL_WRITE);
|
|
cols.push((if w { "✔" } else { "x" }).to_string());
|
|
let x = e.1.contains(PermSet::ACL_EXECUTE);
|
|
cols.push((if x { "✔" } else { "x" }).to_string());
|
|
|
|
Row::new(cols)
|
|
}),
|
|
[
|
|
Constraint::Length(1),
|
|
Constraint::Length(30),
|
|
Constraint::Length(1),
|
|
Constraint::Length(1),
|
|
Constraint::Length(1),
|
|
],
|
|
)
|
|
.header(Row::new(vec!["Type", "Name", "R", "W", "X"]))
|
|
.row_highlight_style(Style::default().bg(Color::Blue).fg(Color::White))
|
|
.highlight_symbol(">>");
|
|
|
|
if self.table_state.selected().is_none() && self.acl.entries.len() > 0 {
|
|
self.table_state.select(Some(0));
|
|
}
|
|
|
|
f.render_stateful_widget(acl_table, rects[0], &mut self.table_state);
|
|
|
|
if let Some(user_popup) = &mut self.user_popup {
|
|
user_popup.render(f, rects[1]);
|
|
} else if let Some(group_popup) = &mut self.group_popup {
|
|
group_popup.render(f, rects[1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ACLEditor {
|
|
pub fn new(path: impl Into<PathBuf>) -> Self {
|
|
let path: PathBuf = path.into();
|
|
|
|
let acl = PosixACL::new_from_file(&path, false).unwrap();
|
|
|
|
Self {
|
|
path,
|
|
acl,
|
|
..Self::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
trait PermissionObject: Clone + std::fmt::Debug + Send + Sync {
|
|
fn get_name(&self) -> String;
|
|
}
|
|
|
|
impl PermissionObject for User {
|
|
fn get_name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
}
|
|
|
|
impl PermissionObject for Group {
|
|
fn get_name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
}
|
|
|
|
struct SelectUserOrGroupPopup<T: PermissionObject> {
|
|
search: String,
|
|
available_objects: Vec<T>,
|
|
selected: Option<T>,
|
|
exited: bool,
|
|
table_state: TableState,
|
|
}
|
|
|
|
impl SelectUserOrGroupPopup<User> {
|
|
fn new_users() -> Self {
|
|
Self {
|
|
search: String::new(),
|
|
available_objects: get_users().unwrap(),
|
|
selected: None,
|
|
table_state: TableState::default(),
|
|
exited: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SelectUserOrGroupPopup<Group> {
|
|
fn new_groups() -> Self {
|
|
Self {
|
|
search: String::new(),
|
|
available_objects: get_groups().unwrap(),
|
|
selected: None,
|
|
table_state: TableState::default(),
|
|
exited: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: PermissionObject> SelectUserOrGroupPopup<T> {
|
|
fn exited(&self) -> bool {
|
|
self.exited
|
|
}
|
|
|
|
fn get_result(&self) -> Option<T> {
|
|
self.selected.clone()
|
|
}
|
|
}
|
|
|
|
impl<T: PermissionObject> SelectUserOrGroupPopup<T> {
|
|
fn get_filtered(&self) -> Vec<T> {
|
|
let search = self.search.to_lowercase();
|
|
self.available_objects
|
|
.iter()
|
|
.filter(|obj| obj.get_name().to_lowercase().contains(&search))
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
impl<T: PermissionObject> Component for SelectUserOrGroupPopup<T> {
|
|
fn update(&mut self, mut event: super::AppEventClient) {
|
|
if event.has_focus() && !event.was_handled() {
|
|
if event.is_focused_and_key_pressed(KeyCode::Up, None) {
|
|
event.mark_as_handled();
|
|
self.table_state.select_previous();
|
|
} else if event.is_focused_and_key_pressed(KeyCode::Down, None) {
|
|
event.mark_as_handled();
|
|
self.table_state.select_next();
|
|
} else if event.is_focused_and_key_pressed(KeyCode::Tab, None) {
|
|
event.mark_as_handled();
|
|
} else if event.is_focused_and_key_pressed(KeyCode::Enter, None) {
|
|
// TODO: Handle selection
|
|
event.mark_as_handled();
|
|
self.selected = self
|
|
.table_state
|
|
.selected()
|
|
.and_then(|idx| self.get_filtered().get(idx).cloned());
|
|
event.has_focus = false;
|
|
self.exited = true;
|
|
} else if let Some(evt) = &event.event {
|
|
if let Event::Key(key) = evt {
|
|
match key.code {
|
|
KeyCode::Esc => {
|
|
event.mark_as_handled();
|
|
self.selected = None;
|
|
self.exited = true;
|
|
event.has_focus = false;
|
|
}
|
|
KeyCode::Char(c) => {
|
|
if key.modifiers == KeyModifiers::empty() {
|
|
self.search.push(c);
|
|
event.mark_as_handled();
|
|
}
|
|
}
|
|
KeyCode::Backspace => {
|
|
self.search.pop();
|
|
event.mark_as_handled();
|
|
}
|
|
KeyCode::Tab => {
|
|
event.mark_as_handled();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if event.has_focus() {
|
|
event.register_helper("Ctrl + q", "Select user");
|
|
event.register_helper("Esc", "Close popup");
|
|
event.register_helper("↑", "Select previous entry");
|
|
event.register_helper("↓", "Select next entry");
|
|
event.register_helper("Enter", "Select entry");
|
|
event.register_helper("Tab", "Switch focus");
|
|
}
|
|
|
|
if self.table_state.selected().is_none() && self.get_filtered().len() > 0 {
|
|
self.table_state.select(Some(0));
|
|
}
|
|
}
|
|
|
|
fn render(&mut self, frame: &mut Frame, _rect: ratatui::prelude::Rect) {
|
|
let area = frame.area();
|
|
|
|
let popup_area = popup_area(area, 50, 50);
|
|
frame.render_widget(Clear, popup_area);
|
|
|
|
let block = Block::bordered()
|
|
.title("Select User or Group")
|
|
.borders(ratatui::widgets::Borders::ALL)
|
|
.border_style(ratatui::style::Style::default().fg(ratatui::style::Color::Cyan));
|
|
let inner_area = block.inner(popup_area);
|
|
frame.render_widget(block, popup_area);
|
|
|
|
let vertical = Layout::vertical([Constraint::Length(3), Constraint::Fill(1)]);
|
|
let [input_area, content_area] = vertical.areas(inner_area);
|
|
|
|
let input = Paragraph::new(self.search.as_str())
|
|
.block(
|
|
Block::default()
|
|
.title("Search")
|
|
.borders(ratatui::widgets::Borders::ALL),
|
|
)
|
|
.style(Style::default().fg(Color::White).bg(Color::Black));
|
|
|
|
frame.render_widget(input, input_area);
|
|
|
|
let content = Table::new(
|
|
self.get_filtered().iter().map(|user| {
|
|
let mut cols = vec![];
|
|
cols.push(user.get_name());
|
|
Row::new(cols)
|
|
}),
|
|
[Constraint::Fill(1)],
|
|
)
|
|
.header(Row::new(vec!["Name"]))
|
|
.row_highlight_style(Style::default().bg(Color::Blue).fg(Color::White))
|
|
.highlight_symbol(">>");
|
|
|
|
frame.render_stateful_widget(content, content_area, &mut self.table_state);
|
|
}
|
|
}
|
|
|
|
fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
|
|
let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
|
|
let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
|
|
let [area] = vertical.areas(area);
|
|
let [area] = horizontal.areas(area);
|
|
area
|
|
}
|