Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 async fn run_server_test<F, Fut>(pool: SqlitePool, test_fn: F)
where
F: FnOnce(String) -> Fut,
Fut: Future<Output = ()>,
Expand Down
156 changes: 156 additions & 0 deletions backend/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,159 @@ pub fn create_router(
let router = router.with_state(state);
Ok(router)
}

#[cfg(test)]
mod tests {
use std::panic;

use chrono::TimeDelta;
use hyper::{Method, header::COOKIE};
use sqlx::SqliteConnection;
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