From de541634df74283557045d07321947906c1023c3 Mon Sep 17 00:00:00 2001 From: Ana Bujan Date: Tue, 8 Mar 2022 15:59:45 +0100 Subject: [PATCH] Add examples, tests, improve documentation, move to stable --- Cargo.toml | 2 +- README.md | 50 +++++++++++++++++-- example/Cargo.toml | 12 ----- examples/hello-world/Cargo.toml | 10 ++++ examples/hello-world/README.md | 7 +++ {example => examples/hello-world}/src/main.rs | 2 - examples/role-based-authorization/Cargo.toml | 18 +++++++ examples/role-based-authorization/README.md | 12 +++++ .../role-based-authorization/src/bin/main.rs | 49 ++++++++++++++++++ examples/role-based-authorization/src/lib.rs | 3 ++ .../role-based-authorization/src/models.rs | 28 +++++++++++ .../src/permissions.rs | 23 +++++++++ .../role-based-authorization/src/routes.rs | 32 ++++++++++++ src/lib.rs | 5 +- src/tests/mod.rs | 1 + src/tests/test_service.rs | 41 +++++++++++++++ tests/test_permission.rs | 9 ---- 17 files changed, 274 insertions(+), 30 deletions(-) delete mode 100644 example/Cargo.toml create mode 100644 examples/hello-world/Cargo.toml create mode 100644 examples/hello-world/README.md rename {example => examples/hello-world}/src/main.rs (96%) create mode 100644 examples/role-based-authorization/Cargo.toml create mode 100644 examples/role-based-authorization/README.md create mode 100644 examples/role-based-authorization/src/bin/main.rs create mode 100644 examples/role-based-authorization/src/lib.rs create mode 100644 examples/role-based-authorization/src/models.rs create mode 100644 examples/role-based-authorization/src/permissions.rs create mode 100644 examples/role-based-authorization/src/routes.rs create mode 100644 src/tests/mod.rs create mode 100644 src/tests/test_service.rs delete mode 100644 tests/test_permission.rs diff --git a/Cargo.toml b/Cargo.toml index b28c3db..27b3d74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-permissions" -version = "0.1.0-beta.1" +version = "0.1.0" edition = "2018" authors = ["Ana Bujan "] readme = "README.md" diff --git a/README.md b/README.md index 2e1989d..78ea590 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,34 @@ -# Actix Permissions [![Continuous Integration](https://github.com/eisberg-labs/actix-permissions/actions/workflows/ci.yml/badge.svg)](https://github.com/eisberg-labs/actix-permissions/actions/workflows/ci.yml) [![cargo-badge][]][cargo] [![license-badge][]][license] [![rust-version-badge][]][rust-version] +# Actix Permissions [![Continuous Integration](https://github.com/eisberg-labs/actix-permissions/actions/workflows/ci.yml/badge.svg)](https://github.com/eisberg-labs/actix-permissions/actions/workflows/ci.yml) [![cargo-badge][]][cargo] [![license-badge][]][license] Permission and input validation extension for Actix Web. Alternative to actix guard, with access to app data injections, HttpRequest and Payload. +Permissions are flexible, take a look at [Examples directory](./examples) for some use cases. + +You could write a permission check like a function or like a struct. +This code: +```rust +fn is_allowed( + req: &HttpRequest, + payload: &mut Payload, +) -> Ready> { + todo!(); +} +``` +is same as writing: +```rust +struct IsAllowed; + +impl Permission for IsAllowed { + fn call(&self, req: &HttpRequest, _payload: &mut Payload) -> Ready> { + todo!(); + } +} +``` # Example Dependencies: ```toml [dependencies] -actix-permissions = "0.1.0-beta.1" +actix-permissions = "0.1.0" ``` Code: ```rust @@ -66,11 +88,31 @@ async fn main() -> std::io::Result<()> { .await } ``` +## Use Cases +Take a look at [Examples directory](./examples). +You could use actix-permissions for role based authorization check, like in *role-based-authorization* example. +*hello-world* example is just proof of concept, showing how you can compose a list of permissions, +access service request, payload and injected services. + +## Contributing + +This project welcomes all kinds of contributions. No contribution is too small! + +If you want to contribute to this project but don't know how to begin or if you need help with something related to this project, +feel free to send me an email (contact form at the bottom). + +Some pointers on contribution are in [Contributing.md](./CONTRIBUTING.md) + +## Code of Conduct + +This project follows the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct). + + +# License +Distributed under the terms of [MIT license](./LICENSE-MIT) and [Apache license](./LICENSE-APACHE). [cargo-badge]: https://img.shields.io/crates/v/actix-permissions.svg?style=flat-square [cargo]: https://crates.io/crates/actix-permissions [license-badge]: https://img.shields.io/badge/license-MIT/Apache--2.0-lightgray.svg?style=flat-square [license]: #license -[rust-version-badge]: https://img.shields.io/badge/rust-1.15+-blue.svg?style=flat-square -[rust-version]: .travis.yml#L5 diff --git a/example/Cargo.toml b/example/Cargo.toml deleted file mode 100644 index 286ff90..0000000 --- a/example/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "actix-permissions-example" -publish = false -version = "0.1.0-SNAPSHOT" -edition = "2018" - -[dependencies] -actix-permissions = { path = ".." } -actix-web = { version = "4.0.1" } -serde_json = { version = "1.0" } -serde = { version = "1", features = ["derive"] } -thiserror = "1.0" diff --git a/examples/hello-world/Cargo.toml b/examples/hello-world/Cargo.toml new file mode 100644 index 0000000..f08dd0d --- /dev/null +++ b/examples/hello-world/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "hello-world-example" +publish = false +version = "0.1.0-SNAPSHOT" +edition = "2018" + +[dependencies] +actix-permissions = { path = "../.." } +actix-web = { version = "4.0.1" } +thiserror = "1.0" diff --git a/examples/hello-world/README.md b/examples/hello-world/README.md new file mode 100644 index 0000000..e2af1c8 --- /dev/null +++ b/examples/hello-world/README.md @@ -0,0 +1,7 @@ +# Hello World Example + +In this example showing how you can compose a list of permissions, +access service request, payload and injected services. + +# Running the App +```cargo run``` and go to , then try . diff --git a/example/src/main.rs b/examples/hello-world/src/main.rs similarity index 96% rename from example/src/main.rs rename to examples/hello-world/src/main.rs index 92c712f..8ce79f5 100644 --- a/example/src/main.rs +++ b/examples/hello-world/src/main.rs @@ -11,7 +11,6 @@ fn dummy_permission_check( // Unecessary complicating permission check to show what it can do. // You have access to request, payload, and all injected dependencies through app_data. let checker_service: Option<&Data> = req.app_data::>(); - // TODO: do not unwrap here ready(Ok(checker_service.unwrap().check(req.query_string()))) } @@ -22,7 +21,6 @@ fn another_dummy_permission_check( // Unecessary complicating permission check to show what it can do. // You have access to request, payload, and all injected dependencies through app_data. let checker_service: Option<&Data> = req.app_data::>(); - // TODO: do not unwrap here ready(Ok(checker_service.unwrap().check(req.query_string()))) } diff --git a/examples/role-based-authorization/Cargo.toml b/examples/role-based-authorization/Cargo.toml new file mode 100644 index 0000000..39f4606 --- /dev/null +++ b/examples/role-based-authorization/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "role-based-authorization-example" +publish = false +version = "0.1.0-SNAPSHOT" +edition = "2018" + +[dependencies] +actix-permissions = { path = "../.." } +actix-web = { version = "4.0.1" } +actix-web-httpauth = "0.6.0" +thiserror = "1.0" + +[lib] +path = "src/lib.rs" + +[[bin]] +path = "src/bin/main.rs" +name = "ums-server" diff --git a/examples/role-based-authorization/README.md b/examples/role-based-authorization/README.md new file mode 100644 index 0000000..5cd0011 --- /dev/null +++ b/examples/role-based-authorization/README.md @@ -0,0 +1,12 @@ +# Role Base Authorization Example + +In this example for role based permission check, basic authentication is used with 3 users. +Each user has a different role - *Administrator, Moderator and User*. + +There are 3 pages served: +- Only for Administrators `admin:1` +- For Moderators and higher `moderator:2` +- For Logged in users `user:3` + +# Running the App +```cargo run``` and go to diff --git a/examples/role-based-authorization/src/bin/main.rs b/examples/role-based-authorization/src/bin/main.rs new file mode 100644 index 0000000..015a437 --- /dev/null +++ b/examples/role-based-authorization/src/bin/main.rs @@ -0,0 +1,49 @@ +use std::fmt::Debug; +use actix_web::{App, HttpServer, ResponseError, HttpMessage}; +use actix_web::web; +use actix_web::http::StatusCode; +use actix_web::dev::ServiceRequest; +use actix_web_httpauth::extractors::basic::BasicAuth; +use actix_web_httpauth::middleware::HttpAuthentication; + +use role_based_authorization_example::routes::routes; +use thiserror::Error; +use role_based_authorization_example::models::User; + +#[derive(Debug, Error)] +pub enum ValidatorError { + #[error("Forbidden")] + Forbidden +} + +impl ResponseError for ValidatorError { + fn status_code(&self) -> StatusCode { + match self { + Self::Forbidden => StatusCode::FORBIDDEN, + } + } +} + +async fn validator(req: ServiceRequest, credentials: BasicAuth) -> Result { + let users = User::list(); + let user = users.iter().find(|it| + credentials.user_id().eq(&it.username) && + credentials.password().is_some() && + credentials.password().unwrap().eq(&it.password)); + if let Some(user) = user { + req.extensions_mut().insert(user.role); + return Ok(req); + } + + Err(ValidatorError::Forbidden.into()) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + let auth = HttpAuthentication::basic(validator); + App::new() + .wrap(auth) + .service(web::scope("").configure(routes)) + }).bind("127.0.0.1:8888")?.run().await +} diff --git a/examples/role-based-authorization/src/lib.rs b/examples/role-based-authorization/src/lib.rs new file mode 100644 index 0000000..8977dfc --- /dev/null +++ b/examples/role-based-authorization/src/lib.rs @@ -0,0 +1,3 @@ +pub mod permissions; +pub mod models; +pub mod routes; diff --git a/examples/role-based-authorization/src/models.rs b/examples/role-based-authorization/src/models.rs new file mode 100644 index 0000000..a718e2d --- /dev/null +++ b/examples/role-based-authorization/src/models.rs @@ -0,0 +1,28 @@ +#[derive(Clone, PartialOrd, PartialEq, Copy)] +pub enum Role { + Administrator, Moderator, User +} + +pub struct User { + pub username: String, + pub role: Role, + pub password: String +} + +impl User { + pub fn new(username: &str, role: Role, password: &str) -> Self { + Self { + username: username.to_string(), + role, + password: password.to_string() + } + } + + pub fn list()->Vec{ + vec![ + User::new("admin", Role::Administrator, "1"), + User::new("moderator", Role::Moderator, "2"), + User::new("user", Role::User, "3"), + ] + } +} diff --git a/examples/role-based-authorization/src/permissions.rs b/examples/role-based-authorization/src/permissions.rs new file mode 100644 index 0000000..8a36214 --- /dev/null +++ b/examples/role-based-authorization/src/permissions.rs @@ -0,0 +1,23 @@ +use actix_permissions::permission::Permission; +use actix_web::dev::Payload; +use actix_web::{HttpMessage, HttpRequest}; +use std::future::{ready, Ready}; +use crate::models::Role; + +#[derive(Clone)] +pub struct RolePermissionCheck { + role: Role, +} + +impl Permission for RolePermissionCheck { + fn call(&self, req: &HttpRequest, _payload: &mut Payload) -> Ready> { + let is_allowed = req.extensions().get::().map(|user_role| self.role >= *user_role).unwrap_or(false); + let res: actix_web::Result = Ok(is_allowed); + ready(res) + } +} + +/// Returns true if logged in user's role is equal or higher than role +pub fn has_min_role(role: Role) -> RolePermissionCheck { + RolePermissionCheck { role } +} diff --git a/examples/role-based-authorization/src/routes.rs b/examples/role-based-authorization/src/routes.rs new file mode 100644 index 0000000..7aec190 --- /dev/null +++ b/examples/role-based-authorization/src/routes.rs @@ -0,0 +1,32 @@ +use actix_permissions::{check, with}; +use actix_web::*; +use actix_web::web::ServiceConfig; + +use crate::models::Role; +use crate::permissions::*; + +async fn administrators_index() -> Result { + Ok("Only for administrators!".to_string()) +} + +async fn moderators_index() -> Result { + Ok("Only for administrators and moderators!".to_string()) +} + +async fn index() -> Result { + Ok("For logged in users!".to_string()) +} + +pub fn routes(cfg: &mut ServiceConfig) { + cfg.route( + "/", + check(web::get(), with(has_min_role(Role::User)), index, ), + ).route( + "/admin", + check(web::get(), with(has_min_role(Role::Administrator)), administrators_index, ), + ) + .route( + "/mod", + check(web::get(), with(has_min_role(Role::Moderator)), moderators_index, ), + ); +} diff --git a/src/lib.rs b/src/lib.rs index 4208a28..06e2277 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ //! //! #[actix_web::main] //! async fn main() -> std::io::Result<()> { -//! +//! //! HttpServer::new(|| { //! App::new() //! .service(web::scope("").route( @@ -42,8 +42,9 @@ //! ``` //! mod builder; +mod tests; pub mod permission; -mod service; +pub(crate) mod service; use crate::builder::Builder; use crate::permission::Permission; diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..5d0cca6 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1 @@ +mod test_service; diff --git a/src/tests/test_service.rs b/src/tests/test_service.rs new file mode 100644 index 0000000..33d3a32 --- /dev/null +++ b/src/tests/test_service.rs @@ -0,0 +1,41 @@ +#[cfg(test)] +mod tests { + use std::future::{ready, Ready}; + use std::sync::Arc; + use actix_web::{Error, HttpRequest, test}; + use actix_web::dev::{Payload, Service}; + use crate::PermissionService; + + async fn index() -> Result { + Ok("Welcome!".to_string()) + } + + + #[actix_web::test] + async fn test_no_permission_checks_set() { + let service_req = test::TestRequest::with_uri("/").to_srv_request(); + let service = PermissionService::new(Arc::new(vec![]), index); + + let result = service.call(service_req).await; + + assert!(result.is_ok()) + } + + + fn deny_all( + _req: &HttpRequest, + _payload: &mut Payload, + ) -> Ready> { + ready(Ok(false)) + } + + #[actix_web::test] + async fn test_deny_all() { + let service_req = test::TestRequest::with_uri("/").to_srv_request(); + let service = PermissionService::new(Arc::new(vec![Box::new(deny_all)]), index); + + let result = service.call(service_req).await; + + assert!(result.is_ok()) + } +} diff --git a/tests/test_permission.rs b/tests/test_permission.rs deleted file mode 100644 index ef64270..0000000 --- a/tests/test_permission.rs +++ /dev/null @@ -1,9 +0,0 @@ -// #[macro_use] -// extern crate serde_derive; -// -// use serde::Serialize; -// -// #[actix_web::test] -// async fn dummy_test() { -// assert!(true); -// }