Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion src/omni_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const UNAUTHORIZED_MESSAGE: &str = "Unauthorized";
const BAD_REQUEST: &str = "Bad Request";
const INSUFFICIENT_PERMISSIONS_MESSAGE: &str =
"You don't have permissions required to perform this operation";
const ROLES_PARSING_MESSAGE: &str = "Failed to parse user roles";
const REFERRING_TO_A_NONEXISTENT_RESOURCE: &str = "Referring to a nonexistent resource";

#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -49,7 +50,9 @@ pub enum OmniError {
BadRequestError,
#[error("{INSUFFICIENT_PERMISSIONS_MESSAGE}")]
InsufficientPermissionsError,
#[error("REFERRING_TO_A_NONEXISTENT_RESOURCE")]
#[error("{ROLES_PARSING_MESSAGE}")]
RolesParsingError,
#[error("{REFERRING_TO_A_NONEXISTENT_RESOURCE}")]
ReferringToNonexistentResourceError,
}

Expand Down Expand Up @@ -149,6 +152,9 @@ impl OmniError {
E::InsufficientPermissionsError => {
(StatusCode::FORBIDDEN, self.clerr()).into_response()
}
E::RolesParsingError => {
(StatusCode::BAD_REQUEST, self.clerr()).into_response()
}
E::ReferringToNonexistentResourceError => {
(StatusCode::NOT_FOUND, self.clerr()).into_response()
}
Expand All @@ -173,6 +179,7 @@ impl OmniError {
E::UnauthorizedError => UNAUTHORIZED_MESSAGE,
E::BadRequestError => BAD_REQUEST,
E::InsufficientPermissionsError => INSUFFICIENT_PERMISSIONS_MESSAGE,
E::RolesParsingError => ROLES_PARSING_MESSAGE,
E::ReferringToNonexistentResourceError => REFERRING_TO_A_NONEXISTENT_RESOURCE,
}
.to_string()
Expand Down
1 change: 0 additions & 1 deletion src/routes/debate_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use axum::{
routing::get,
Json, Router,
};
use sqlx::query_as;
use tower_cookies::Cookies;
use tracing::error;
use uuid::Uuid;
Expand Down
2 changes: 2 additions & 0 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod health_check;
mod infradmin_routes;
mod location_routes;
mod motion_routes;
mod roles_routes;
mod room_routes;
mod swagger;
mod team_routes;
Expand All @@ -33,4 +34,5 @@ pub fn routes() -> Router<AppState> {
.merge(user_routes::route())
.merge(location_routes::route())
.merge(room_routes::route())
.merge(roles_routes::route())
}
269 changes: 269 additions & 0 deletions src/routes/roles_routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
use axum::{
extract::{Path, State},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
routing::post,
Json, Router,
};
use serde_json::Error;
use strum::VariantArray;
use tower_cookies::Cookies;
use tracing::error;
use uuid::Uuid;

use crate::{
omni_error::OmniError,
setup::AppState,
tournament::roles::{Role, RoleVecExt},
users::{permissions::Permission, TournamentUser, User},
};

pub fn route() -> Router<AppState> {
Router::new().route(
"/user/:user_id/tournament/:tournament_id/roles",
post(create_user_roles)
.get(get_user_roles)
.patch(patch_user_roles)
.delete(delete_user_roles),
)
}

/// Grant roles to a user
///
/// Available only to Organizers and and the infrastructure admin.
#[utoipa::path(
post,
request_body=Vec<Role>,
path = "/user/{user_id}/tournament/{tournament_id}/roles",
responses(
(
status=200, description = "Roles created successfully",
body=Vec<Role>
),
(status=400, description = "Bad request"),
(
status=401,
description = "The user is not permitted to modify roles within this tournament"
),
(status=404, description = "User of tournament not found"),
(status=409, description = "The user is already granted roles within this tournament. Use PATCH method to modify user roles"),
(status=500, description = "Internal server error"),
),
tag = "roles"
)]
async fn create_user_roles(
State(state): State<AppState>,
headers: HeaderMap,
cookies: Cookies,
Path((user_id, tournament_id)): Path<(Uuid, Uuid)>,
Json(json): Json<Vec<Role>>,
) -> Result<Response, OmniError> {
let pool = &state.connection_pool;
let tournament_user =
TournamentUser::authenticate(tournament_id, &headers, cookies, pool).await?;

match tournament_user.has_permission(Permission::WriteRoles) {
true => (),
false => return Err(OmniError::UnauthorizedError),
}

let target_user = User::get_by_id(user_id, pool).await?;
let roles = target_user.get_roles(tournament_id, pool).await?;
if !roles.is_empty() {
return Err(OmniError::ResourceAlreadyExistsError);
}

match Role::post(user_id, tournament_id, json, pool).await {
Ok(role) => Ok(Json(role).into_response()),
Err(e) => {
error!(
"Error creating roles for user {} within tournament {}: {e}",
user_id, tournament_id
);
Err(e)
}
}
}

/// List roles a user is given within a tournament
///
/// The user must be given a role within this tournament to use this endpoint.
#[utoipa::path(get, path = "/user/{user_id}/tournament/{tournament_id}/roles",
responses(
(status=200, description = "Ok", body=Vec<Role>,
example=json!(get_roles_example())
),
(status=400, description="Bad request"),
(status=401, description="The user is not permitted to see roles, meaning they don't have any role within this tournament"),
(status=404, description="User or tournament not found"),
(status=500, description="Internal server error"),
),
tag = "roles"
)]
async fn get_user_roles(
State(state): State<AppState>,
headers: HeaderMap,
cookies: Cookies,
Path((user_id, tournament_id)): Path<(Uuid, Uuid)>,
) -> Result<Response, OmniError> {
let pool = &state.connection_pool;
let tournament_user =
TournamentUser::authenticate(tournament_id, &headers, cookies, &pool).await?;

if tournament_user.roles.is_empty() {
return Err(OmniError::UnauthorizedError);
}

let requested_user = User::get_by_id(user_id, pool).await?;
match requested_user.get_roles(tournament_id, pool).await {
Ok(roles) => Ok(Json(roles as Vec<Role>).into_response()),
Err(e) => {
error!(
"Error getting roles of user {} within tournament {}: {e}",
user_id, tournament_id
);
Err(e)
}
}
}

/// Overwrite roles a user is given within a tournament
///
/// Available only to the tournament Organizers and the infrastructure admin.
#[utoipa::path(patch, path = "/user/{user_id}/tournament/{tournament_id}/roles",
request_body=Vec<Role>,
responses(
(
status=200, description = "Roles patched successfully",
body=Vec<Role>,
example=json!(get_roles_example())
),
(status=400, description = "Bad request"),
(
status=401,
description = "The user is not permitted to modify roles within this tournament"
),
(status=404, description = "Tournament or user not found, or the user has not been assigned any roles yet"),
(status=500, description = "Internal server error"),
),
tag = "roles"
)]
async fn patch_user_roles(
State(state): State<AppState>,
headers: HeaderMap,
cookies: Cookies,
Path((user_id, tournament_id)): Path<(Uuid, Uuid)>,
Json(new_roles): Json<Vec<Role>>,
) -> Result<Response, OmniError> {
let pool = &state.connection_pool;
let tournament_user =
TournamentUser::authenticate(tournament_id, &headers, cookies, pool).await?;

match tournament_user.has_permission(Permission::WriteRoles) {
true => (),
false => return Err(OmniError::UnauthorizedError),
}

let modified_user = TournamentUser::get_by_id(user_id, tournament_id, pool).await?;
if modified_user.roles.is_empty() {
return Err(OmniError::ResourceNotFoundError);
}

match Role::patch(user_id, tournament_id, new_roles, pool).await {
Ok(roles) => Ok(Json(roles).into_response()),
Err(e) => {
error!(
"Error patching roles of user {} within tournament {}: {e}",
user_id, tournament_id
);
Err(e)
}
}
}

/// Delete user roles within a tournament
/// This operation effectively means banning the user from a tournament.
/// Available only to the tournament Organizers and the infrastructure admin.
#[utoipa::path(delete, path = "/user/{user_id}/tournament/{tournament_id}/roles",
responses
(
(status=204, description = "Roles deleted successfully"),
(status=400, description = "Bad request"),
(
status=401,
description = "The user is not permitted to modify roles within this tournament"
),
(status=404, description = "User or tournament not found"),
(status=500, description = "Internal server error"),
),
tag = "roles"
)]
async fn delete_user_roles(
State(state): State<AppState>,
headers: HeaderMap,
cookies: Cookies,
Path((user_id, tournament_id)): Path<(Uuid, Uuid)>,
) -> Result<Response, OmniError> {
let pool = &state.connection_pool;
let tournament_user =
TournamentUser::authenticate(tournament_id, &headers, cookies, pool).await?;

match tournament_user.has_permission(Permission::WriteRoles) {
true => (),
false => return Err(OmniError::UnauthorizedError),
}

match Role::delete(user_id, tournament_id, pool).await {
Ok(_) => Ok(StatusCode::NO_CONTENT.into_response()),
Err(e) => {
error!(
"Error deleting roles of user {} within tournament {}: {e}",
user_id, tournament_id
);
Err(e)
}
}
}

fn get_roles_example() -> String {
r#"
["Marshall", "Judge"]
"#
.to_owned()
}

#[test]
fn role_to_string() {
let judge = Role::Judge;
let marshall = Role::Marshall;
let organizer = Role::Organizer;

assert!(serde_json::to_string(&judge).unwrap() == "\"Judge\"");
assert!(serde_json::to_string(&marshall).unwrap() == "\"Marshall\"");
assert!(serde_json::to_string(&organizer).unwrap() == "\"Organizer\"");
}

#[test]
fn role_vecs_to_string() {
let roles = Role::VARIANTS.to_vec();
let roles_count = roles.len();
let roles_as_strings = roles.to_string_vec();
for i in 0..roles_count {
assert!(roles_as_strings[i] == roles[i].to_string())
}
}

#[test]
fn string_to_roles() {
let valid_roles = Role::VARIANTS.to_vec();
let fake_role = "\"Gżdacz\"";

for role in valid_roles {
let serialized_role = serde_json::to_string(&role).unwrap();
let deserialized_role: Role = serde_json::from_str(&serialized_role).unwrap();
assert!(role == deserialized_role);
}

let deserialized_fake_role: Result<Role, Error> = serde_json::from_str(fake_role);
assert!(deserialized_fake_role.is_err());
}
Loading