Compare commits

...

2 Commits

Author SHA1 Message Date
29bfa8c60e
Added function to get all roles of members 2025-02-07 17:03:27 +01:00
31aa9dc066
Revamed auth system 2025-02-07 15:59:50 +01:00
15 changed files with 127 additions and 128 deletions

1
server/Cargo.lock generated
View File

@ -2203,6 +2203,7 @@ checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4"
dependencies = [ dependencies = [
"getrandom 0.2.15", "getrandom 0.2.15",
"rand 0.8.5", "rand 0.8.5",
"serde",
] ]
[[package]] [[package]]

View File

@ -23,7 +23,7 @@ bitflags = { version = "2.8", features = [ "serde" ] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
chrono = "0.4" chrono = "0.4"
uuid = { version = "1.12", features = ["v4", "fast-rng"] } uuid = { version = "1.12", features = ["v4", "fast-rng", "serde"] }
serde_json = "1.0.137" serde_json = "1.0.137"
rand = "0.9" rand = "0.9"
rand_chacha = "0.9" rand_chacha = "0.9"

View File

@ -4,7 +4,7 @@ CREATE TABLE "members" (
full_name text NOT NULL, full_name text NOT NULL,
registration_token text NOT NULL UNIQUE, registration_token text NOT NULL UNIQUE,
diploma text, diploma text,
swim_groups bigint NOT NULL, groups bigint NOT NULL,
groups bigint NOT NULL roles bigint NOT NULL
); );

View File

@ -1,75 +1,60 @@
use std::collections::HashSet;
use argon2::{ use argon2::{
password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
Argon2, PasswordHash, PasswordVerifier, Argon2, PasswordHash, PasswordVerifier,
}; };
use axum::{ use axum::http::{header, HeaderMap};
extract::FromRequestParts, use chrono::Utc;
http::{request::Parts, StatusCode},
RequestPartsExt,
};
use axum_extra::{
extract::cookie::{Cookie, CookieJar},
headers::{authorization::Bearer, Authorization},
typed_header::TypedHeaderRejectionReason,
TypedHeader,
};
use bearer::verify_bearer;
pub use error::AuthError; pub use error::AuthError;
use rand::distr::Alphanumeric; use rand::distr::Alphanumeric;
use rand::prelude::*; use rand::prelude::*;
use rand_chacha::ChaCha20Rng; use rand_chacha::ChaCha20Rng;
use sqlx::PgPool;
use tokio::task; use tokio::task;
use crate::database::model::User; use crate::{
database::model::{Session, UserMember},
model::{member::Roles, User},
};
mod bearer;
mod error; mod error;
mod scopes;
#[derive(Debug)] pub async fn get_user_from_header(
pub struct Permissions<'a>(pub HashSet<&'a str>); pool: &PgPool,
headers: &HeaderMap,
) -> Result<(Roles, User), AuthError> {
let bearer_value = headers.get(header::AUTHORIZATION);
let bearer_value = bearer_value
.ok_or_else(|| AuthError::InvalidToken)?
.to_str()
.map_err(|_| AuthError::InvalidToken)?;
// Middleware for getting permissions let token = get_token_from_bearer(bearer_value)?;
impl<S> FromRequestParts<S> for Permissions<'_>
where
S: Send + Sync,
{
type Rejection = crate::Error;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { let session = match Session::from_token(&pool, &token).await {
// First check if the request has a beaerer token to authenticate Ok(s) => s,
match parts.extract::<TypedHeader<Authorization<Bearer>>>().await { Err(_) => return Err(AuthError::InvalidToken),
Ok(bearer) => { };
verify_bearer(bearer.token().to_string()).map_err(|_| AuthError::InvalidToken)?;
let permissions = Permissions { if session.expires_at < Utc::now() {
0: HashSet::from(["root"]), return Err(AuthError::InvalidToken);
}; }
return Ok(permissions); let db_user = match crate::database::model::User::get(&pool, session.user_id).await {
} Ok(u) => u,
Err(err) => match err.reason() { Err(_) => return Err(AuthError::InvalidToken),
TypedHeaderRejectionReason::Missing => (), };
TypedHeaderRejectionReason::Error(_err) => {
return Err(AuthError::InvalidToken.into())
}
_ => return Err(AuthError::Unexpected.into()),
},
};
match parts.extract::<CookieJar>().await { let roles = UserMember::get_roles(&pool, &db_user.user_id)
Ok(jar) => { .await
if let Some(session_token) = jar.get("session_token") { .unwrap_or(Roles::MEMBER);
// TODO: Implement function to retrieve user permissions
tracing::info!("{session_token:?}")
}
}
Err(_) => (),
}
Err(AuthError::Unauthorized.into()) Ok((roles, db_user.into()))
}
pub fn get_token_from_bearer(bearer: &str) -> Result<String, AuthError> {
match bearer.strip_prefix("Bearer ") {
Some(token) => Ok(token.to_string()),
None => return Err(AuthError::InvalidToken),
} }
} }

View File

@ -1,8 +0,0 @@
pub fn verify_bearer(token: String) -> Result<(), ()> {
let env_api_token = dotenvy::var("API_TOKEN").map_err(|_| ())?;
match env_api_token == token {
true => Ok(()),
false => Err(()),
}
}

View File

@ -1,19 +0,0 @@
use crate::bitflags_serde_impl;
use bitflags::bitflags;
use serde::Deserialize;
bitflags! {
#[derive(Clone, Copy, Debug)]
pub struct Scopes: u64 {
const USER_READ = 1 << 0;
const USER_WRITE = 1 << 1;
const USER_DELETE = 1 << 2;
const MEMBER_CREATE = 1 << 3;
const MEMBER_READ = 1 << 4;
const MEMBER_WRITE = 1 << 5;
const MEMBER_DELETE = 1 << 6;
}
}
bitflags_serde_impl!(Scopes, u64);

View File

@ -2,7 +2,7 @@ use rand::distr::{Alphanumeric, SampleString};
use sqlx::{PgPool, Postgres, QueryBuilder}; use sqlx::{PgPool, Postgres, QueryBuilder};
use validator::Validate; use validator::Validate;
use crate::model::member::{Groups, SwimGroups}; use crate::model::member::{Groups, Roles};
#[derive(Debug, Validate, sqlx::FromRow)] #[derive(Debug, Validate, sqlx::FromRow)]
pub struct Member { pub struct Member {
@ -12,8 +12,8 @@ pub struct Member {
pub full_name: String, pub full_name: String,
pub registration_token: Option<String>, pub registration_token: Option<String>,
pub diploma: Option<String>, pub diploma: Option<String>,
pub swim_groups: SwimGroups,
pub groups: Groups, pub groups: Groups,
pub roles: Roles,
} }
impl Member { impl Member {
@ -53,7 +53,7 @@ impl Member {
} }
let mut query_builder = QueryBuilder::new( let mut query_builder = QueryBuilder::new(
"INSERT INTO members(member_id, first_name, full_name, registration_token, diploma, swim_groups, groups) " "INSERT INTO members(member_id, first_name, full_name, registration_token, diploma, groups, roles) "
); );
query_builder.push_values(members.into_iter(), |mut b, member| { query_builder.push_values(members.into_iter(), |mut b, member| {
@ -64,8 +64,8 @@ impl Member {
b.push_bind(member.full_name); b.push_bind(member.full_name);
b.push_bind(registration_token); b.push_bind(registration_token);
b.push_bind(member.diploma); b.push_bind(member.diploma);
b.push_bind(member.swim_groups.bits() as i64);
b.push_bind(member.groups.bits() as i64); b.push_bind(member.groups.bits() as i64);
b.push_bind(member.roles.bits() as i64);
}); });
let query = query_builder.build(); let query = query_builder.build();
@ -86,14 +86,14 @@ impl Member {
sqlx::query!( sqlx::query!(
" "
UPDATE ONLY members UPDATE ONLY members
SET first_name = $1, full_name = $2, diploma = $3, swim_groups = $4, groups = $5 SET first_name = $1, full_name = $2, diploma = $3, groups = $4, roles = $5
WHERE member_id = $6 WHERE member_id = $6
", ",
member.first_name, member.first_name,
member.full_name, member.full_name,
member.diploma, member.diploma,
member.swim_groups.bits() as i64,
member.groups.bits() as i64, member.groups.bits() as i64,
member.roles.bits() as i64,
member.member_id member.member_id
) )
.execute(&mut **transaction) .execute(&mut **transaction)

View File

@ -1,6 +1,7 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use sqlx::{PgPool, Postgres}; use sqlx::{PgPool, Postgres};
#[derive(Debug)]
pub struct Session { pub struct Session {
pub session_id: uuid::Uuid, pub session_id: uuid::Uuid,
pub user_id: uuid::Uuid, pub user_id: uuid::Uuid,

View File

@ -1,5 +1,7 @@
use sqlx::{PgPool, Postgres}; use sqlx::{PgPool, Postgres};
use crate::model::member::Roles;
#[derive(validator::Validate)] #[derive(validator::Validate)]
pub struct User { pub struct User {
pub user_id: uuid::Uuid, pub user_id: uuid::Uuid,
@ -44,6 +46,14 @@ impl User {
Ok(user) Ok(user)
} }
pub async fn get(transaction: &PgPool, user_id: uuid::Uuid) -> Result<Self, sqlx::Error> {
let user = sqlx::query_as!(Self, "SELECT * FROM users WHERE user_id = $1", user_id)
.fetch_one(transaction)
.await?;
Ok(user)
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -73,4 +83,20 @@ impl UserMember {
Ok(()) Ok(())
} }
pub async fn get_roles(pool: &PgPool, user_id: &uuid::Uuid) -> Result<Roles, sqlx::Error> {
let roles = sqlx::query_scalar!(
"
SELECT roles FROM users_members INNER JOIN members ON users_members.member_id = members.member_id AND users_members.user_id = $1;
",
user_id
).fetch_all(pool).await?;
let roles: Vec<Roles> = roles.into_iter().map(|r| r.into()).collect();
let roles = roles
.into_iter()
.fold(Roles::empty(), |acc, flag| acc | flag);
Ok(roles)
}
} }

View File

@ -13,21 +13,22 @@ pub struct Member {
pub name: Name, pub name: Name,
pub registration_token: Option<String>, pub registration_token: Option<String>,
pub diploma: Option<String>, pub diploma: Option<String>,
pub swim_groups: SwimGroups,
pub groups: Groups, pub groups: Groups,
pub roles: Roles,
} }
bitflags! { bitflags! {
#[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Groups: u64 { pub struct Roles: u64 {
const NONE = 1 << 0; const MEMBER = 1 << 0;
const KADER = 1 << 1; const KADER = 1 << 1;
const ZWEMZAKEN = 1 << 2; const ZWEMZAKEN = 1 << 2;
const WEDSTRIJDEN = 1 << 3; const WEDSTRIJDEN = 1 << 3;
const ADMIN = 1 << 4;
} }
#[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct SwimGroups: u64 { pub struct Groups: u64 {
const NONE = 1 << 0; const NONE = 1 << 0;
const A1 = 1 << 1; const A1 = 1 << 1;
@ -76,15 +77,15 @@ bitflags! {
} }
} }
impl From<i64> for SwimGroups { impl From<i64> for Groups {
fn from(value: i64) -> Self { fn from(value: i64) -> Self {
Self::from_bits(value as u64).unwrap_or(SwimGroups::NONE) Self::from_bits(value as u64).unwrap_or(Groups::empty())
} }
} }
impl From<i64> for Groups { impl From<i64> for Roles {
fn from(value: i64) -> Self { fn from(value: i64) -> Self {
Self::from_bits(value as u64).unwrap_or(Groups::NONE) Self::from_bits(value as u64).unwrap_or(Roles::MEMBER)
} }
} }
@ -99,8 +100,8 @@ impl From<DbMember> for Member {
}, },
registration_token: value.registration_token, registration_token: value.registration_token,
diploma: value.diploma, diploma: value.diploma,
swim_groups: value.swim_groups,
groups: value.groups, groups: value.groups,
roles: value.roles,
} }
} }
} }
@ -113,8 +114,8 @@ impl From<Member> for DbMember {
full_name: value.name.full, full_name: value.name.full,
registration_token: None, registration_token: None,
diploma: value.diploma, diploma: value.diploma,
swim_groups: value.swim_groups,
groups: value.groups, groups: value.groups,
roles: value.roles,
} }
} }
} }

View File

@ -2,6 +2,7 @@ use chrono::{DateTime, Duration, Utc};
use crate::auth::generate_session_token; use crate::auth::generate_session_token;
#[derive(Debug)]
pub struct Session { pub struct Session {
pub session_id: uuid::Uuid, pub session_id: uuid::Uuid,
pub user_id: uuid::Uuid, pub user_id: uuid::Uuid,

View File

@ -1,5 +1,19 @@
use serde::Serialize;
#[derive(Serialize)]
pub struct User { pub struct User {
pub id: uuid::Uuid, pub id: uuid::Uuid,
pub email: String, pub email: String,
pub admin: bool, pub admin: bool,
} }
use crate::database::model::User as DbUser;
impl From<DbUser> for User {
fn from(db_user: DbUser) -> Self {
Self {
id: db_user.user_id,
email: db_user.email,
admin: db_user.admin,
}
}
}

View File

@ -1,5 +1,5 @@
use crate::{auth::Permissions, AppState}; use crate::{auth::get_user_from_header, model::User, AppState};
use axum::{extract::State, http::StatusCode, routing::get, Router}; use axum::{extract::State, http::HeaderMap, routing::get, Json, Router};
pub mod auth; pub mod auth;
pub mod member; pub mod member;
@ -14,10 +14,10 @@ pub fn routes() -> Router<AppState> {
} }
async fn root( async fn root(
State(_state): State<AppState>, State(state): State<AppState>,
permissions: Permissions<'_>, headers: HeaderMap,
) -> Result<String, (StatusCode, String)> { ) -> Result<Json<User>, crate::Error> {
tracing::info!("{:?}", permissions); let (_roles, user) = get_user_from_header(&state.pool, &headers).await?;
Ok("Hello world".to_string()) Ok(Json(user))
} }

View File

@ -1,6 +1,6 @@
use axum::{extract::State, routing::post, Router}; use axum::{extract::State, routing::post, Router};
use crate::{auth::Permissions, AppState}; use crate::AppState;
pub mod migrate; pub mod migrate;
@ -12,7 +12,6 @@ pub fn routes() -> Router<AppState> {
pub async fn get_members<'a>( pub async fn get_members<'a>(
State(state): State<AppState>, State(state): State<AppState>,
permissions: Permissions<'a>,
body: String, body: String,
) -> Result<(), crate::Error> { ) -> Result<(), crate::Error> {
Ok(()) Ok(())

View File

@ -2,16 +2,17 @@ use std::collections::HashMap;
use axum::{ use axum::{
extract::{FromRef, State}, extract::{FromRef, State},
http::HeaderMap,
Json, Json,
}; };
use itertools::Itertools; use itertools::Itertools;
use sqlx::PgPool; use sqlx::PgPool;
use crate::{ use crate::{
auth::{AuthError, Permissions}, auth::{get_user_from_header, AuthError},
database::model::Member as DbMember, database::model::Member as DbMember,
model::{ model::{
member::{Groups, Name, SwimGroups}, member::{Groups, Name, Roles},
Member, Member,
}, },
util::convert_vec, util::convert_vec,
@ -20,10 +21,12 @@ use crate::{
pub async fn migrate_request<'a>( pub async fn migrate_request<'a>(
State(state): State<AppState>, State(state): State<AppState>,
permissions: Permissions<'a>, headers: HeaderMap,
body: String, body: String,
) -> Result<Json<MigrationResponse>, crate::Error> { ) -> Result<Json<MigrationResponse>, crate::Error> {
if !permissions.0.contains("root") { let (roles, _user) = get_user_from_header(&state.pool, &headers).await?;
if !roles.contains(Roles::ADMIN) {
return Err(AuthError::NoPermssions.into()); return Err(AuthError::NoPermssions.into());
} }
@ -50,13 +53,8 @@ pub async fn migrate_request<'a>(
pub async fn migrate_confirm<'a>( pub async fn migrate_confirm<'a>(
State(state): State<AppState>, State(state): State<AppState>,
permissions: Permissions<'a>,
body: String, body: String,
) -> Result<(), crate::Error> { ) -> Result<(), crate::Error> {
if !permissions.0.contains("root") {
return Err(AuthError::NoPermssions.into());
}
tracing::info!("Migration is confirmed"); tracing::info!("Migration is confirmed");
let count = match body.trim().parse::<u32>() { let count = match body.trim().parse::<u32>() {
@ -119,7 +117,7 @@ struct Row {
#[serde(rename = "E-mail")] #[serde(rename = "E-mail")]
email: String, email: String,
#[serde(rename = "Verenigingssporten")] #[serde(rename = "Verenigingssporten")]
swim_groups: String, groups: String,
#[serde(rename = "Diploma dropdown 1")] #[serde(rename = "Diploma dropdown 1")]
diploma: Option<String>, diploma: Option<String>,
} }
@ -164,22 +162,22 @@ impl Row {
members members
} }
fn swim_groups_parsed(&self) -> SwimGroups { fn groups_parsed(&self) -> Groups {
let mut swim_groups: Vec<String> = Vec::new(); let mut groups: Vec<String> = Vec::new();
let group_parts: Vec<&str> = self.swim_groups.split(", ").collect(); let group_parts: Vec<&str> = self.groups.split(", ").collect();
for group in group_parts { for group in group_parts {
let hour_parts: Vec<&str> = group.split(" - ").collect(); let hour_parts: Vec<&str> = group.split(" - ").collect();
if let Some(group) = hour_parts.get(1) { if let Some(group) = hour_parts.get(1) {
swim_groups.push(group.to_uppercase()) groups.push(group.to_uppercase())
} }
} }
let swim_groups_string = swim_groups.join("|"); let groups_string = groups.join("|");
bitflags::parser::from_str(&swim_groups_string).unwrap_or(SwimGroups::empty()) bitflags::parser::from_str(&groups_string).unwrap_or(Groups::empty())
} }
} }
@ -201,8 +199,8 @@ impl Into<Member> for Row {
name, name,
registration_token: None, registration_token: None,
diploma: self.diploma.clone(), diploma: self.diploma.clone(),
swim_groups: self.swim_groups_parsed(), groups: self.groups_parsed(),
groups: Groups::empty(), roles: Roles::MEMBER,
} }
} }
} }
@ -264,8 +262,8 @@ fn generate_diff(members_new: Vec<Member>, members_old: Vec<Member>) -> MembersD
name: new_member.name.clone(), name: new_member.name.clone(),
registration_token: old_member.registration_token, registration_token: old_member.registration_token,
diploma: new_member.diploma.clone(), diploma: new_member.diploma.clone(),
swim_groups: new_member.swim_groups.clone(), groups: new_member.groups,
groups: old_member.groups, roles: old_member.roles,
}) })
} else { } else {
members_remove.push(old_member); members_remove.push(old_member);