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
|
# 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).
|
# 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 http::StatusCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{postgres::PgRow, Error, FromRow, PgPool, Row};
|
use sqlx::{postgres::PgRow, Error, FromRow, PgPool, Row};
|
||||||
use tracing::event;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use rbac::RbacService;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
middlewares::is_authorized,
|
middlewares::is_authorized, rbac, user::{get_user_roles_display, AccountData}
|
||||||
user::{get_user_roles_display, AccountData},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct HtmlTemplate<T>(T);
|
struct HtmlTemplate<T>(T);
|
||||||
|
|
@ -52,17 +51,32 @@ struct CalendarTemplate {
|
||||||
|
|
||||||
pub async fn calendar(
|
pub async fn calendar(
|
||||||
Extension(user_data): Extension<Option<AccountData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
Extension(rbac): Extension<RbacService>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
let mut is_authorized = false;
|
||||||
|
|
||||||
if logged_in {
|
if logged_in {
|
||||||
// Extract the user data.
|
// 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 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
|
// Get user roles
|
||||||
let user_roles = get_user_roles_display(userid, &db_pool.clone()).await;
|
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>,
|
State(db_pool): State<PgPool>,
|
||||||
Query(params): Query<EventParams>,
|
Query(params): Query<EventParams>,
|
||||||
Extension(user_data): Extension<Option<AccountData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
Extension(rbac): Extension<RbacService>,
|
||||||
) -> String {
|
) -> String {
|
||||||
//println!("Paramters: {:?}", params);
|
//println!("Paramters: {:?}", params);
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
|
|
@ -146,15 +161,48 @@ pub async fn get_events(
|
||||||
|
|
||||||
// Extract the user data.
|
// 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 userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||||
|
|
||||||
if is_authorized("/calendar", user_data, db_pool.clone()).await {
|
// Set empty query string
|
||||||
// User is authorized
|
let mut query = r#""#;
|
||||||
//println!("User is authorized");
|
|
||||||
|
|
||||||
// Get requested calendar events from database
|
if !rbac.has_permission(userid, "calendar:admin:*").await {
|
||||||
let events = sqlx::query(
|
if !rbac.has_permission(userid, "calendar:read:*").await {
|
||||||
r#"select to_json(json_agg(json_build_object(
|
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,
|
'title', ce.title,
|
||||||
'start', ce.start_time,
|
'start', ce.start_time,
|
||||||
'end', ce.end_time,
|
'end', ce.end_time,
|
||||||
|
|
@ -166,10 +214,33 @@ pub async fn get_events(
|
||||||
where ce.celebrate = true
|
where ce.celebrate = true
|
||||||
and c.name = 'Cottage'
|
and c.name = 'Cottage'
|
||||||
and start_time > $1
|
and start_time > $1
|
||||||
and start_time < $2"#,
|
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,
|
||||||
|
'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"#;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.start).unwrap())
|
||||||
.bind(chrono::DateTime::parse_from_rfc3339(¶ms.end).unwrap())
|
.bind(chrono::DateTime::parse_from_rfc3339(¶ms.end).unwrap())
|
||||||
|
.bind(userid)
|
||||||
.fetch_one(&db_pool)
|
.fetch_one(&db_pool)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|
@ -220,6 +291,7 @@ pub struct EventCreate {
|
||||||
pub async fn create_event(
|
pub async fn create_event(
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<AccountData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
Extension(rbac): Extension<RbacService>,
|
||||||
Form(event): Form<EventCreate>,
|
Form(event): Form<EventCreate>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if is_authorized("/calendar", user_data.clone(), db_pool.clone()).await {
|
if is_authorized("/calendar", user_data.clone(), db_pool.clone()).await {
|
||||||
|
|
@ -264,26 +336,39 @@ pub struct NewRequest {
|
||||||
pub async fn new_request(
|
pub async fn new_request(
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<AccountData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
Extension(rbac): Extension<RbacService>,
|
||||||
request: axum::http::Request<axum::body::Body>,
|
request: axum::http::Request<axum::body::Body>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
let mut is_authorized = false;
|
||||||
|
|
||||||
// Set default events
|
// Set default events
|
||||||
let mut eventstring: String = "[]".to_string();
|
let mut eventstring: String = "[]".to_string();
|
||||||
|
|
||||||
if logged_in {
|
if logged_in {
|
||||||
// User is logged in
|
|
||||||
//println!("User is logged in");
|
|
||||||
|
|
||||||
// Extract the user data.
|
// 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 userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||||
let personid = user_data
|
let personid = user_data
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|s| s.person_id.clone())
|
.map(|s| s.person_id.clone())
|
||||||
.unwrap_or_default();
|
.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 (_parts, body) = request.into_parts();
|
||||||
let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||||
let body_str = String::from_utf8(bytes.to_vec()).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 {
|
let user_id = if let Ok(query) = query {
|
||||||
query.0
|
query.0
|
||||||
} else {
|
} else {
|
||||||
// Add user
|
// Add person
|
||||||
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"#)
|
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(email.clone())
|
||||||
.bind(name.clone())
|
.bind(name.clone())
|
||||||
.bind(family_name.clone())
|
.bind(family_name.clone())
|
||||||
|
|
@ -202,9 +202,19 @@ pub async fn google_auth_return(
|
||||||
.fetch_one(&db_pool)
|
.fetch_one(&db_pool)
|
||||||
.await?;
|
.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
|
// 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'))"#)
|
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)
|
.execute(&db_pool)
|
||||||
.await?;
|
.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);
|
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);
|
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
|
// Update session with user id or create new session
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ mod routes;
|
||||||
mod secret_gift_exchange;
|
mod secret_gift_exchange;
|
||||||
mod user;
|
mod user;
|
||||||
mod wishlist;
|
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 error_handling::AppError;
|
||||||
use google_oauth::{google_auth_return, login, logout};
|
use google_oauth::{google_auth_return, login, logout};
|
||||||
use middlewares::inject_user_data;
|
use middlewares::inject_user_data;
|
||||||
|
|
@ -32,7 +34,6 @@ use wishlist::{
|
||||||
user_wishlist_returned_item, user_wishlist_save_item, wishlists,
|
user_wishlist_returned_item, user_wishlist_save_item, wishlists,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::calendar::new_request;
|
|
||||||
//use email::send_emails;
|
//use email::send_emails;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -71,6 +72,8 @@ async fn main() {
|
||||||
println!("SOURCE_DB_URL not set");
|
println!("SOURCE_DB_URL not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rbac = RbacService::new(app_state.db_pool.clone());
|
||||||
|
|
||||||
let user_data: Option<AccountData> = None;
|
let user_data: Option<AccountData> = None;
|
||||||
|
|
||||||
// build our application with some routes
|
// build our application with some routes
|
||||||
|
|
@ -141,7 +144,8 @@ async fn main() {
|
||||||
inject_user_data,
|
inject_user_data,
|
||||||
))
|
))
|
||||||
.with_state(app_state.db_pool.clone())
|
.with_state(app_state.db_pool.clone())
|
||||||
.layer(Extension(user_data));
|
.layer(Extension(user_data))
|
||||||
|
.layer(Extension(rbac));
|
||||||
|
|
||||||
// Send email indicating server has started
|
// Send email indicating server has started
|
||||||
//let recipients = get_useremails_by_role("admin".to_string(), &app_state.db_pool).await;
|
//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