Initial RBAC for calendar use
This commit is contained in:
parent
e9edd3e82a
commit
904cd4315c
|
|
@ -1,4 +1,4 @@
|
|||
FROM mcr.microsoft.com/devcontainers/rust:1-1-bookworm
|
||||
FROM mcr.microsoft.com/devcontainers/rust:latest
|
||||
|
||||
# Include lld linker to improve build times either by using environment variable
|
||||
# RUSTFLAGS="-C link-arg=-fuse-ld=lld" or with Cargo's configuration file (i.e see .cargo/config.toml).
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,12 +8,11 @@ use chrono::Days;
|
|||
use http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{postgres::PgRow, Error, FromRow, PgPool, Row};
|
||||
use tracing::event;
|
||||
use uuid::Uuid;
|
||||
use rbac::RbacService;
|
||||
|
||||
use crate::{
|
||||
middlewares::is_authorized,
|
||||
user::{get_user_roles_display, AccountData},
|
||||
middlewares::is_authorized, rbac, user::{get_user_roles_display, AccountData}
|
||||
};
|
||||
|
||||
struct HtmlTemplate<T>(T);
|
||||
|
|
@ -52,17 +51,32 @@ struct CalendarTemplate {
|
|||
|
||||
pub async fn calendar(
|
||||
Extension(user_data): Extension<Option<AccountData>>,
|
||||
Extension(rbac): Extension<RbacService>,
|
||||
State(db_pool): State<PgPool>,
|
||||
) -> impl IntoResponse {
|
||||
// Is the user logged in?
|
||||
let logged_in = user_data.is_some();
|
||||
let mut is_authorized = false;
|
||||
|
||||
if logged_in {
|
||||
// Extract the user data.
|
||||
let user = user_data.as_ref().unwrap().clone();
|
||||
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||
|
||||
if is_authorized("/calendar", user_data, db_pool.clone()).await {
|
||||
if !rbac.has_permission(userid, "calendar:admin:*").await {
|
||||
if !rbac.has_permission(userid, "calendar:read:*").await {
|
||||
if !rbac.has_permission(userid, "calendar:personal:*").await {
|
||||
} else {
|
||||
is_authorized = true;
|
||||
}
|
||||
} else {
|
||||
is_authorized = true;
|
||||
}
|
||||
} else {
|
||||
is_authorized = true;
|
||||
}
|
||||
|
||||
if is_authorized {
|
||||
// Get user roles
|
||||
let user_roles = get_user_roles_display(userid, &db_pool.clone()).await;
|
||||
|
||||
|
|
@ -132,6 +146,7 @@ pub async fn get_events(
|
|||
State(db_pool): State<PgPool>,
|
||||
Query(params): Query<EventParams>,
|
||||
Extension(user_data): Extension<Option<AccountData>>,
|
||||
Extension(rbac): Extension<RbacService>,
|
||||
) -> String {
|
||||
//println!("Paramters: {:?}", params);
|
||||
// Is the user logged in?
|
||||
|
|
@ -146,15 +161,63 @@ pub async fn get_events(
|
|||
|
||||
// Extract the user data.
|
||||
let _user = user_data.as_ref().unwrap().clone();
|
||||
let _userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||
|
||||
if is_authorized("/calendar", user_data, db_pool.clone()).await {
|
||||
// User is authorized
|
||||
//println!("User is authorized");
|
||||
// Set empty query string
|
||||
let mut query = r#""#;
|
||||
|
||||
// Get requested calendar events from database
|
||||
let events = sqlx::query(
|
||||
r#"select to_json(json_agg(json_build_object(
|
||||
if !rbac.has_permission(userid, "calendar:admin:*").await {
|
||||
if !rbac.has_permission(userid, "calendar:read:*").await {
|
||||
if !rbac.has_permission(userid, "calendar:personal:*").await {
|
||||
} else {
|
||||
query = r#"select to_json(json_agg(jbo.val))
|
||||
from (select json_build_object(
|
||||
'title', ce.title,
|
||||
'start', ce.start_time,
|
||||
'end', ce.end_time,
|
||||
'allDay', false,
|
||||
'backgroundColor', cet.colour)
|
||||
from calendar_events ce
|
||||
join calendar c on c.id = ce.calendar_id
|
||||
join calendar_event_types cet on cet.id = ce.event_type_id
|
||||
where ce.celebrate = true
|
||||
and c.name = 'Cottage'
|
||||
and start_time > $1
|
||||
and start_time < $2
|
||||
and ce.created_by = $3
|
||||
union all
|
||||
select json_build_object(
|
||||
'title', 'In use',
|
||||
'start', ce.start_time,
|
||||
'end', ce.end_time,
|
||||
'allDay', false,
|
||||
'backgroundColor', cet.colour)
|
||||
from calendar_events ce
|
||||
join calendar c on c.id = ce.calendar_id
|
||||
join calendar_event_types cet on cet.id = ce.event_type_id
|
||||
where ce.celebrate = true
|
||||
and c.name = 'Cottage'
|
||||
and start_time > $1
|
||||
and start_time < $2
|
||||
and ce.created_by != $3) as jbo(val)"#;
|
||||
}
|
||||
} else {
|
||||
query = r#"select to_json(json_agg(json_build_object(
|
||||
'title', ce.title,
|
||||
'start', ce.start_time,
|
||||
'end', ce.end_time,
|
||||
'allDay', false,
|
||||
'backgroundColor', cet.colour)))
|
||||
from calendar_events ce
|
||||
join calendar c on c.id = ce.calendar_id
|
||||
join calendar_event_types cet on cet.id = ce.event_type_id
|
||||
where ce.celebrate = true
|
||||
and c.name = 'Cottage'
|
||||
and start_time > $1
|
||||
and start_time < $2"#;
|
||||
}
|
||||
} else {
|
||||
query = r#"select to_json(json_agg(json_build_object(
|
||||
'title', ce.title,
|
||||
'start', ce.start_time,
|
||||
'end', ce.end_time,
|
||||
|
|
@ -166,10 +229,18 @@ pub async fn get_events(
|
|||
where ce.celebrate = true
|
||||
and c.name = 'Cottage'
|
||||
and start_time > $1
|
||||
and start_time < $2"#,
|
||||
)
|
||||
and start_time < $2"#;
|
||||
}
|
||||
|
||||
if query.len() > 0 {
|
||||
// User is authorized
|
||||
//println!("User is authorized");
|
||||
|
||||
// Get requested calendar events from database
|
||||
let events = sqlx::query(query,)
|
||||
.bind(chrono::DateTime::parse_from_rfc3339(¶ms.start).unwrap())
|
||||
.bind(chrono::DateTime::parse_from_rfc3339(¶ms.end).unwrap())
|
||||
.bind(userid)
|
||||
.fetch_one(&db_pool)
|
||||
.await;
|
||||
|
||||
|
|
@ -220,6 +291,7 @@ pub struct EventCreate {
|
|||
pub async fn create_event(
|
||||
State(db_pool): State<PgPool>,
|
||||
Extension(user_data): Extension<Option<AccountData>>,
|
||||
Extension(rbac): Extension<RbacService>,
|
||||
Form(event): Form<EventCreate>,
|
||||
) -> impl IntoResponse {
|
||||
if is_authorized("/calendar", user_data.clone(), db_pool.clone()).await {
|
||||
|
|
@ -264,26 +336,39 @@ pub struct NewRequest {
|
|||
pub async fn new_request(
|
||||
State(db_pool): State<PgPool>,
|
||||
Extension(user_data): Extension<Option<AccountData>>,
|
||||
Extension(rbac): Extension<RbacService>,
|
||||
request: axum::http::Request<axum::body::Body>,
|
||||
) -> impl IntoResponse {
|
||||
// Is the user logged in?
|
||||
let logged_in = user_data.is_some();
|
||||
let mut is_authorized = false;
|
||||
|
||||
// Set default events
|
||||
let mut eventstring: String = "[]".to_string();
|
||||
|
||||
if logged_in {
|
||||
// User is logged in
|
||||
//println!("User is logged in");
|
||||
|
||||
// Extract the user data.
|
||||
let _user = user_data.as_ref().unwrap().clone();
|
||||
let user = user_data.as_ref().unwrap().clone();
|
||||
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||
let personid = user_data
|
||||
.as_ref()
|
||||
.map(|s| s.person_id.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
if is_authorized("/calendar", user_data, db_pool.clone()).await {
|
||||
if !rbac.has_permission(userid, "calendar:admin:*").await {
|
||||
if !rbac.has_permission(userid, "calendar:read:*").await {
|
||||
if !rbac.has_permission(userid, "calendar:personal:*").await {
|
||||
} else {
|
||||
is_authorized = true;
|
||||
}
|
||||
} else {
|
||||
is_authorized = true;
|
||||
}
|
||||
} else {
|
||||
is_authorized = true;
|
||||
}
|
||||
|
||||
if is_authorized {
|
||||
let (_parts, body) = request.into_parts();
|
||||
let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
let body_str = String::from_utf8(bytes.to_vec()).unwrap();
|
||||
|
|
|
|||
|
|
@ -193,8 +193,8 @@ pub async fn google_auth_return(
|
|||
let user_id = if let Ok(query) = query {
|
||||
query.0
|
||||
} else {
|
||||
// Add user
|
||||
let query: (uuid::Uuid,) = sqlx::query_as(r#"INSERT INTO users (created_by, updated_by, email, name, family_name, given_name) VALUES ((SELECT id FROM users WHERE "name" = 'admin'), (SELECT id FROM users WHERE "name" = 'admin'), $1, $2, $3, $4) RETURNING id"#)
|
||||
// Add person
|
||||
let person: (uuid::Uuid,) = sqlx::query_as(r#"INSERT INTO people (created_by, updated_by, email, name, family_name, given_name) VALUES ((SELECT id FROM users WHERE "name" = 'admin'), (SELECT id FROM users WHERE "name" = 'admin'), $1, $2, $3, $4) RETURNING id"#)
|
||||
.bind(email.clone())
|
||||
.bind(name.clone())
|
||||
.bind(family_name.clone())
|
||||
|
|
@ -202,9 +202,19 @@ pub async fn google_auth_return(
|
|||
.fetch_one(&db_pool)
|
||||
.await?;
|
||||
|
||||
// Add user
|
||||
let user: (uuid::Uuid,) = sqlx::query_as(r#"INSERT INTO users (created_by, updated_by, email, name, family_name, given_name, person_id) VALUES ((SELECT id FROM users WHERE "name" = 'admin'), (SELECT id FROM users WHERE "name" = 'admin'), $1, $2, $3, $4, $5) RETURNING id"#)
|
||||
.bind(email.clone())
|
||||
.bind(name.clone())
|
||||
.bind(family_name.clone())
|
||||
.bind(given_name.clone())
|
||||
.bind(person.0)
|
||||
.fetch_one(&db_pool)
|
||||
.await?;
|
||||
|
||||
// Add public role
|
||||
sqlx::query(r#"INSERT INTO user_roles (created_by, updated_by, user_id, role_id) VALUES ((SELECT id FROM users WHERE "name" = 'admin'), (SELECT id FROM users WHERE "name" = 'admin'), $1, (SELECT id FROM roles WHERE "name" = 'public'))"#)
|
||||
.bind(query.0)
|
||||
.bind(user.0)
|
||||
.execute(&db_pool)
|
||||
.await?;
|
||||
|
||||
|
|
@ -213,7 +223,7 @@ pub async fn google_auth_return(
|
|||
let body = format!("A new user has registered on the website: <br> <b>Name:</b> {} <br> <b>Email:</b> {} <br> <b>Family Name:</b> {} <br> <b>Given Name:</b> {}", name, email, family_name, given_name);
|
||||
send_emails("Jean-Marie website - New user registration".to_string(), recipients, body);
|
||||
|
||||
query.0
|
||||
user.0
|
||||
};
|
||||
|
||||
// Update session with user id or create new session
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ mod routes;
|
|||
mod secret_gift_exchange;
|
||||
mod user;
|
||||
mod wishlist;
|
||||
mod rbac;
|
||||
|
||||
use calendar::{calendar, get_events, create_event, new_event};
|
||||
use rbac::RbacService;
|
||||
use calendar::{calendar, get_events, create_event, new_event, new_request};
|
||||
use error_handling::AppError;
|
||||
use google_oauth::{google_auth_return, login, logout};
|
||||
use middlewares::inject_user_data;
|
||||
|
|
@ -32,7 +34,6 @@ use wishlist::{
|
|||
user_wishlist_returned_item, user_wishlist_save_item, wishlists,
|
||||
};
|
||||
|
||||
use crate::calendar::new_request;
|
||||
//use email::send_emails;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -71,6 +72,8 @@ async fn main() {
|
|||
println!("SOURCE_DB_URL not set");
|
||||
}
|
||||
|
||||
let rbac = RbacService::new(app_state.db_pool.clone());
|
||||
|
||||
let user_data: Option<AccountData> = None;
|
||||
|
||||
// build our application with some routes
|
||||
|
|
@ -141,7 +144,8 @@ async fn main() {
|
|||
inject_user_data,
|
||||
))
|
||||
.with_state(app_state.db_pool.clone())
|
||||
.layer(Extension(user_data));
|
||||
.layer(Extension(user_data))
|
||||
.layer(Extension(rbac));
|
||||
|
||||
// Send email indicating server has started
|
||||
//let recipients = get_useremails_by_role("admin".to_string(), &app_state.db_pool).await;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
// src/rbac.rs
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive (Clone)]
|
||||
pub struct RbacService {
|
||||
pool: sqlx::PgPool,
|
||||
}
|
||||
|
||||
impl RbacService {
|
||||
pub fn new(pool: sqlx::PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
pub async fn has_permission(&self, user_id: Uuid, resource: &str) -> bool {
|
||||
let result: Result<Vec<String>, _> = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT rp.item FROM roles r
|
||||
INNER JOIN role_permissions rp ON r.id = rp.role_id
|
||||
INNER JOIN user_roles ur ON r.id = ur.role_id
|
||||
WHERE ur.user_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(patterns) => patterns.iter()
|
||||
.any(|pattern| permission_matches(pattern, resource)),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wildcard permission matching (e.g., "article:edit:*" matches "article:edit:123")
|
||||
fn permission_matches(pattern: &str, resource: &str) -> bool {
|
||||
let pattern_segments: Vec<&str> = pattern.split(':').collect();
|
||||
let resource_segments: Vec<&str> = resource.split(':').collect();
|
||||
|
||||
if pattern_segments.len() != resource_segments.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
pattern_segments.iter()
|
||||
.zip(resource_segments.iter())
|
||||
.all(|(p, r)| p == r || *p == "*")
|
||||
}
|
||||
Loading…
Reference in New Issue