Skip to content
Merged
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
2 changes: 1 addition & 1 deletion backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ mod test {

use super::start_server;

async fn run_server_test<F, Fut>(pool: SqlitePool, test_fn: F)
pub(crate) async fn run_server_test<F, Fut>(pool: SqlitePool, test_fn: F)
where
F: FnOnce(String) -> Fut,
Fut: Future<Output = ()>,
Expand Down
155 changes: 155 additions & 0 deletions backend/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,158 @@ pub fn create_router(
pub fn create_router_without_airgap_detection(pool: SqlitePool) -> Result<Router, AppError> {
create_router(pool, AirgapDetection::nop())
}

#[cfg(test)]
mod tests {
use chrono::TimeDelta;
use hyper::{Method, header::COOKIE};
use sqlx::SqliteConnection;
use std::panic;
use test_log::test;

use crate::{
SqlitePoolExt,
authentication::{Role, session},
test::run_server_test,
};

use super::*;

async fn get_user_cookie(conn: &mut SqliteConnection, user_id: u32) -> String {
session::create(conn, user_id, "", "127.0.0.1", TimeDelta::seconds(60 * 30))
.await
.unwrap()
.get_cookie()
.stripped()
.to_string()
}

#[test(sqlx::test(fixtures(path = "../fixtures", scripts("election_2", "users"))))]
async fn test_route_authorization(pool: SqlitePool) {
let openapi = openapi_router().into_openapi();

// Possible auth-related error statuses
let auth_errors = [StatusCode::UNAUTHORIZED, StatusCode::FORBIDDEN];

// Get cookies for all roles
let mut tx = pool.begin_immediate().await.unwrap();
let auth_states = [
(None, None),
(
Some(Role::Administrator),
Some(get_user_cookie(&mut tx, 1).await),
),
(
Some(Role::Coordinator),
Some(get_user_cookie(&mut tx, 3).await),
),
(Some(Role::Typist), Some(get_user_cookie(&mut tx, 5).await)),
];
tx.commit().await.unwrap();

let client = reqwest::Client::new();
let mut failures = Vec::new();

// Ensure logout is tested last
let mut paths: Vec<_> = openapi.paths.paths.iter().collect();
paths.sort_by_key(|(path, _)| path.contains("logout"));

run_server_test(pool, |base_url| async move {
// Loop through all the paths in the openapi spec
for (path, item) in paths {
let operations = [
(Method::GET, &item.get),
(Method::POST, &item.post),
(Method::PUT, &item.put),
(Method::PATCH, &item.patch),
(Method::DELETE, &item.delete),
];

// Loop through all the operations for each path
for (method, operation) in operations.into_iter() {
// Skip if no operation defined
let Some(operation) = operation else { continue };

// Replace path parameters with (dummy) values
let mut path = path.to_string();
if let Some(parameters) = operation.parameters.as_ref() {
for param in parameters.iter() {
path = path.replace(&format!("{{{}}}", &param.name), "123");
}
}

// Check if scopes are valid
let scopes = get_scopes_from_operation(operation);
if let Some(scopes) = &scopes {
for scope in scopes {
if panic::catch_unwind(|| Role::from(scope.clone())).is_err() {
failures.push(format!(
"- {method} {path} contains invalid scope '{scope}'"
));
}
}
}

// Make requests with and without authentication, for all roles and check the response codes
for auth_state in auth_states.iter() {
let (role, cookie_opt) = auth_state;

let mut request =
client.request(method.clone(), format!("{base_url}{path}"));

if let Some(cookie) = cookie_opt {
request = request.header(COOKIE, cookie);
}

let response = request.send().await.unwrap();
let status = response.status();

// Determine if we expect an auth-related error
let mut expected_error_status = match (&scopes, role) {
// Security defined but no role (unauthenticated)
(Some(_), None) => Some(StatusCode::UNAUTHORIZED),

// Security defined and role given, but not in scopes
(Some(scopes), Some(role))
if !scopes.iter().any(|s| s.eq(&role.to_string())) =>
{
Some(StatusCode::FORBIDDEN)
}

// Else, OK
_ => None,
};

// Exception, this route always forbids access after initialisation
if path == "/api/initialise/admin-exists" {
expected_error_status = Some(StatusCode::FORBIDDEN);
}

match expected_error_status {
// If we expect no error, but got an auth error, record failure
None if auth_errors.contains(&status) => failures.push(format!(
"- {method} {path} as {:?}, got {status}, expected no auth error",
role
)),

// If we expect an error, but got something else than expected, record failure
Some(expected) if status != expected => failures.push(format!(
"- {method} {path} as {:?}, got {status}, expected {expected:?}",
role
)),
_ => {}
}
}
}
}

assert!(
failures.is_empty(),
"Authorization test failures ({}):\n{}",
failures.len(),
failures.join("\n")
);
})
.await;
}
}
137 changes: 0 additions & 137 deletions backend/tests/authorization_test.rs

This file was deleted.

Loading