Added permission handling and MVP status!!!!
parent
c380e71dda
commit
10a6905263
|
@ -408,6 +408,7 @@ dependencies = [
|
||||||
name = "daemon"
|
name = "daemon"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.4.2",
|
||||||
"config",
|
"config",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"j_db",
|
"j_db",
|
||||||
|
|
|
@ -15,6 +15,7 @@ structopt = "0.3.26"
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.11.3"
|
||||||
thiserror = "1.0.57"
|
thiserror = "1.0.57"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
|
bitflags = "2.4.2"
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1.36.0"
|
version = "1.36.0"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use config::Config;
|
use config::Config;
|
||||||
|
use poise::serenity_prelude::UserId;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
@ -12,6 +13,7 @@ pub struct Args {
|
||||||
pub struct DaemonConfig {
|
pub struct DaemonConfig {
|
||||||
pub discord_api_token: String,
|
pub discord_api_token: String,
|
||||||
pub db_path: PathBuf,
|
pub db_path: PathBuf,
|
||||||
|
pub admins: Vec<UserId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DaemonConfig {
|
impl DaemonConfig {
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
use crate::config::DaemonConfig;
|
use crate::config::DaemonConfig;
|
||||||
|
use crate::error::DaemonError;
|
||||||
use crate::models::service::{Service, ServiceGroup};
|
use crate::models::service::{Service, ServiceGroup};
|
||||||
|
use crate::models::user::{Permissions, User, UserGroup};
|
||||||
use j_db::database::Database;
|
use j_db::database::Database;
|
||||||
use j_db::model::JdbModel;
|
use j_db::model::JdbModel;
|
||||||
use log::info;
|
use log::info;
|
||||||
use poise::serenity_prelude::MessageBuilder;
|
use poise::serenity_prelude::{CreateAttachment, Mentionable, MessageBuilder, UserId};
|
||||||
use poise::{serenity_prelude as serenity, ChoiceParameter};
|
use poise::{serenity_prelude as serenity, ChoiceParameter, CreateReply};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct Data {
|
struct Data {
|
||||||
|
@ -14,7 +16,11 @@ struct Data {
|
||||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
type Context<'a> = poise::Context<'a, Data, Error>;
|
type Context<'a> = poise::Context<'a, Data, Error>;
|
||||||
|
|
||||||
#[poise::command(slash_command, ephemeral)]
|
async fn check_owner(ctx: Context<'_>) -> Result<bool, Error> {
|
||||||
|
Ok(ctx.data().config.admins.contains(&ctx.author().id))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[poise::command(slash_command, ephemeral, check = "check_owner")]
|
||||||
async fn add_service(
|
async fn add_service(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Service unit name to add"] service_name: String,
|
#[description = "Service unit name to add"] service_name: String,
|
||||||
|
@ -41,7 +47,7 @@ async fn add_service(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command, ephemeral)]
|
#[poise::command(slash_command, ephemeral, check = "check_owner")]
|
||||||
async fn add_service_group(
|
async fn add_service_group(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "New group name"] group_name: String,
|
#[description = "New group name"] group_name: String,
|
||||||
|
@ -83,6 +89,96 @@ async fn add_service_to_group(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[poise::command(slash_command, ephemeral, check = "check_owner")]
|
||||||
|
async fn add_user(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "User to add to db"] user: UserId,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let user = User::new(user);
|
||||||
|
|
||||||
|
let user = ctx.data().db.insert(user)?;
|
||||||
|
|
||||||
|
let discord_user = user.discord_uuid.to_user(&ctx.http()).await?;
|
||||||
|
ctx.reply(format!("{} has been added.", discord_user.mention()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[poise::command(slash_command, ephemeral, check = "check_owner")]
|
||||||
|
async fn add_user_group(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "New group name"] group_name: String,
|
||||||
|
#[description = "Description of the group"] group_description: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let group = UserGroup::new(&group_name, &group_description);
|
||||||
|
|
||||||
|
ctx.data().db.insert(group)?;
|
||||||
|
|
||||||
|
ctx.reply(format!("Added group `{}`", group_name)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[poise::command(slash_command, ephemeral, check = "check_owner")]
|
||||||
|
async fn add_user_to_group(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "User"] user: UserId,
|
||||||
|
#[description = "User Group"] group_name: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let daemon_user = User::find_user_by_id(&ctx.data().db, user)?;
|
||||||
|
|
||||||
|
if let Some(user) = daemon_user {
|
||||||
|
UserGroup::add_user_to_group(&ctx.data().db, &group_name, user.id().unwrap())?;
|
||||||
|
ctx.reply("Added user to group.").await?;
|
||||||
|
} else {
|
||||||
|
let discord_user = user.to_user(&ctx.http()).await?;
|
||||||
|
ctx.reply(format!(
|
||||||
|
"{} is not a user in the db!",
|
||||||
|
discord_user.mention()
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[poise::command(slash_command, ephemeral, check = "check_owner")]
|
||||||
|
async fn set_user_group_permission(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "User Group"] user_group: String,
|
||||||
|
#[description = "Service Group"] service_group: String,
|
||||||
|
#[description = "Permission"] permissions: u32,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let service_group = ServiceGroup::find_group_by_name(&ctx.data().db, &service_group)?
|
||||||
|
.ok_or(DaemonError::ServiceGroupNotFound)?;
|
||||||
|
|
||||||
|
UserGroup::set_group_permission(
|
||||||
|
&ctx.data().db,
|
||||||
|
&user_group,
|
||||||
|
service_group.id().unwrap(),
|
||||||
|
Permissions::from_bits(permissions).unwrap(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
ctx.reply("User group permission updated.").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[poise::command(slash_command, ephemeral, check = "check_owner")]
|
||||||
|
async fn dump_db(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let db_state = ctx.data().db.dump_db()?;
|
||||||
|
let db_state = db_state.pretty(4);
|
||||||
|
|
||||||
|
ctx.send(
|
||||||
|
CreateReply::default()
|
||||||
|
.attachment(CreateAttachment::bytes(db_state.into_bytes(), "db.json")),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command, ephemeral)]
|
#[poise::command(slash_command, ephemeral)]
|
||||||
async fn list_services(
|
async fn list_services(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
|
@ -93,11 +189,21 @@ async fn list_services(
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
let user = User::find_user_by_id(&ctx.data().db, ctx.author().id)?
|
||||||
|
.ok_or(DaemonError::UserNotAuthorized)?;
|
||||||
|
|
||||||
let services: Vec<String> = ctx
|
let services: Vec<String> = ctx
|
||||||
.data()
|
.data()
|
||||||
.db
|
.db
|
||||||
.filter(|_, _s: &Service| true)?
|
.filter(|_, s: &Service| {
|
||||||
|
UserGroup::check_if_user_has_permission(
|
||||||
|
&ctx.data().db,
|
||||||
|
user.id().unwrap(),
|
||||||
|
s.id().unwrap(),
|
||||||
|
Permissions::Status,
|
||||||
|
)
|
||||||
|
.unwrap_or(false)
|
||||||
|
})?
|
||||||
.filter(|s: &Service| {
|
.filter(|s: &Service| {
|
||||||
if let Some(group) = &group {
|
if let Some(group) = &group {
|
||||||
group.services.contains(&s.id().unwrap())
|
group.services.contains(&s.id().unwrap())
|
||||||
|
@ -108,7 +214,7 @@ async fn list_services(
|
||||||
.map(|s: Service| s.name)
|
.map(|s: Service| s.name)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut message_builder = serenity::MessageBuilder::new();
|
let mut message_builder = MessageBuilder::new();
|
||||||
|
|
||||||
if let Some(group) = group {
|
if let Some(group) = group {
|
||||||
message_builder.push_line(format!("## `{}` Services", group.name));
|
message_builder.push_line(format!("## `{}` Services", group.name));
|
||||||
|
@ -139,6 +245,17 @@ pub enum ServiceAction {
|
||||||
Status,
|
Status,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ServiceAction {
|
||||||
|
pub fn permission(&self) -> Permissions {
|
||||||
|
match self {
|
||||||
|
ServiceAction::Start => Permissions::Start,
|
||||||
|
ServiceAction::Stop => Permissions::Stop,
|
||||||
|
ServiceAction::Restart => Permissions::Restart,
|
||||||
|
ServiceAction::Status => Permissions::Status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command, ephemeral)]
|
#[poise::command(slash_command, ephemeral)]
|
||||||
async fn service(
|
async fn service(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
|
@ -146,8 +263,21 @@ async fn service(
|
||||||
#[description = "Service unit name"] service_name: String,
|
#[description = "Service unit name"] service_name: String,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let service: Option<Service> = Service::find_service_by_name(&ctx.data().db, &service_name)?;
|
let service: Option<Service> = Service::find_service_by_name(&ctx.data().db, &service_name)?;
|
||||||
|
let user = User::find_user_by_id(&ctx.data().db, ctx.author().id)?
|
||||||
|
.ok_or(DaemonError::UserNotAuthorized)?;
|
||||||
|
|
||||||
if let Some(service) = service {
|
if let Some(service) = service {
|
||||||
|
let check_perm = UserGroup::check_if_user_has_permission(
|
||||||
|
&ctx.data().db,
|
||||||
|
user.id().unwrap(),
|
||||||
|
service.id().unwrap(),
|
||||||
|
action.permission(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if !check_perm {
|
||||||
|
return Err(Error::from(DaemonError::UserNotAuthorized));
|
||||||
|
}
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
ServiceAction::Start => {
|
ServiceAction::Start => {
|
||||||
systemctl::start(&service_name)?;
|
systemctl::start(&service_name)?;
|
||||||
|
@ -197,6 +327,11 @@ pub async fn run_bot(db: Database, config: DaemonConfig) {
|
||||||
add_service_group(),
|
add_service_group(),
|
||||||
add_service_to_group(),
|
add_service_to_group(),
|
||||||
service(),
|
service(),
|
||||||
|
add_user(),
|
||||||
|
add_user_group(),
|
||||||
|
add_user_to_group(),
|
||||||
|
dump_db(),
|
||||||
|
set_user_group_permission(),
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum DaemonError {
|
pub enum DaemonError {
|
||||||
#[error("DB error")]
|
#[error("DB error")]
|
||||||
DBError(#[from] j_db::error::JDbError),
|
DBError(#[from] j_db::error::JDbError),
|
||||||
|
#[error("User group not found")]
|
||||||
|
UserGroupNotFound,
|
||||||
|
#[error("Service group not found")]
|
||||||
|
ServiceGroupNotFound,
|
||||||
|
#[error("User not authorized")]
|
||||||
|
UserNotAuthorized,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
pub mod user;
|
||||||
|
|
|
@ -89,6 +89,15 @@ impl ServiceGroup {
|
||||||
.next()
|
.next()
|
||||||
.clone())
|
.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_service_groups(
|
||||||
|
db: &Database,
|
||||||
|
service: u64,
|
||||||
|
) -> Result<Vec<ServiceGroup>, j_db::error::JDbError> {
|
||||||
|
Ok(db
|
||||||
|
.filter(|_, group: &ServiceGroup| group.services.contains(&service))?
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl j_db::model::JdbModel for ServiceGroup {
|
impl j_db::model::JdbModel for ServiceGroup {
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
use crate::models::service::ServiceGroup;
|
||||||
|
use bitflags::bitflags;
|
||||||
|
use j_db::database::Database;
|
||||||
|
use j_db::model::JdbModel;
|
||||||
|
use poise::serenity_prelude::UserId;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
pub discord_uuid: UserId,
|
||||||
|
id: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub fn new(discord_uuid: UserId) -> Self {
|
||||||
|
Self {
|
||||||
|
discord_uuid,
|
||||||
|
id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_user_by_id(
|
||||||
|
db: &Database,
|
||||||
|
uuid: UserId,
|
||||||
|
) -> Result<Option<User>, j_db::error::JDbError> {
|
||||||
|
let user = db
|
||||||
|
.filter(|_, user: &User| user.discord_uuid == uuid)?
|
||||||
|
.next();
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl j_db::model::JdbModel for User {
|
||||||
|
fn id(&self) -> Option<u64> {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_id(&mut self, id: u64) {
|
||||||
|
self.id = Some(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tree() -> String {
|
||||||
|
"User".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
#[derive(Serialize, Deserialize, Copy, Debug, Clone, PartialEq)]
|
||||||
|
pub struct Permissions: u32 {
|
||||||
|
const Status = 0b00001;
|
||||||
|
const Start = 0b00010;
|
||||||
|
const Stop = 0b00100;
|
||||||
|
const Restart = 0b01000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UserGroup {
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub members: HashSet<u64>,
|
||||||
|
pub permissions: HashMap<u64, Permissions>,
|
||||||
|
|
||||||
|
id: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserGroup {
|
||||||
|
pub fn new(name: &str, description: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
description: description.to_string(),
|
||||||
|
members: HashSet::new(),
|
||||||
|
permissions: Default::default(),
|
||||||
|
id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_group_by_name(
|
||||||
|
db: &Database,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Option<Self>, j_db::error::JDbError> {
|
||||||
|
let group = db
|
||||||
|
.filter(|_, group: &UserGroup| group.name.eq_ignore_ascii_case(name))?
|
||||||
|
.next();
|
||||||
|
|
||||||
|
Ok(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_user_to_group(
|
||||||
|
db: &Database,
|
||||||
|
group_name: &str,
|
||||||
|
user_id: u64,
|
||||||
|
) -> Result<Self, j_db::error::JDbError> {
|
||||||
|
let mut group =
|
||||||
|
Self::find_group_by_name(db, group_name)?.ok_or(j_db::error::JDbError::NotFound)?;
|
||||||
|
|
||||||
|
group.members.insert(user_id);
|
||||||
|
|
||||||
|
let group = db.insert(group)?;
|
||||||
|
|
||||||
|
Ok(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_group_permission(
|
||||||
|
db: &Database,
|
||||||
|
group_name: &str,
|
||||||
|
service_group: u64,
|
||||||
|
permissions: Permissions,
|
||||||
|
) -> Result<Self, j_db::error::JDbError> {
|
||||||
|
let mut group =
|
||||||
|
Self::find_group_by_name(db, group_name)?.ok_or(j_db::error::JDbError::NotFound)?;
|
||||||
|
|
||||||
|
group.permissions.insert(service_group, permissions);
|
||||||
|
|
||||||
|
let group = db.insert(group)?;
|
||||||
|
|
||||||
|
Ok(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_user_groups(
|
||||||
|
db: &Database,
|
||||||
|
user: u64,
|
||||||
|
) -> Result<Vec<UserGroup>, j_db::error::JDbError> {
|
||||||
|
Ok(db
|
||||||
|
.filter(|_, group: &UserGroup| group.members.contains(&user))?
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_if_user_has_permission(
|
||||||
|
db: &Database,
|
||||||
|
user: u64,
|
||||||
|
service: u64,
|
||||||
|
permission: Permissions,
|
||||||
|
) -> Result<bool, j_db::error::JDbError> {
|
||||||
|
let user_groups = Self::get_user_groups(db, user)?;
|
||||||
|
let service_group = ServiceGroup::get_service_groups(db, service)?;
|
||||||
|
|
||||||
|
Ok(Self::check_perm(permission, user_groups, service_group))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_perm(
|
||||||
|
permission: Permissions,
|
||||||
|
user_groups: Vec<UserGroup>,
|
||||||
|
service_group: Vec<ServiceGroup>,
|
||||||
|
) -> bool {
|
||||||
|
let mut unified_permission_map: HashMap<u64, Permissions> = HashMap::new();
|
||||||
|
|
||||||
|
for user_group in user_groups {
|
||||||
|
for (group, permission) in user_group.permissions {
|
||||||
|
#[allow(clippy::map_entry)]
|
||||||
|
if unified_permission_map.contains_key(&group) {
|
||||||
|
let perm1 = unified_permission_map.get_mut(&group).unwrap();
|
||||||
|
*perm1 |= permission;
|
||||||
|
} else {
|
||||||
|
unified_permission_map.insert(group, permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let service_group_ids: HashSet<u64> = service_group
|
||||||
|
.iter()
|
||||||
|
.map(|service_group| service_group.id().unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for service_group in service_group_ids {
|
||||||
|
let perm = unified_permission_map.get(&service_group);
|
||||||
|
|
||||||
|
if let Some(perm) = perm {
|
||||||
|
let check_perm = (permission & *perm) == permission;
|
||||||
|
|
||||||
|
if check_perm {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JdbModel for UserGroup {
|
||||||
|
fn id(&self) -> Option<u64> {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_id(&mut self, id: u64) {
|
||||||
|
self.id = Some(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tree() -> String {
|
||||||
|
"UserGroup".to_string()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue