Skip to content

Commit

Permalink
Add examples, tests, improve documentation, move to stable
Browse files Browse the repository at this point in the history
  • Loading branch information
amarjanica committed Mar 8, 2022
1 parent 9edca01 commit de54163
Show file tree
Hide file tree
Showing 17 changed files with 274 additions and 30 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "actix-permissions"
version = "0.1.0-beta.1"
version = "0.1.0"
edition = "2018"
authors = ["Ana Bujan <[email protected]>"]
readme = "README.md"
Expand Down
50 changes: 46 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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<actix_web::Result<bool, actix_web::Error>> {
todo!();
}
```
is same as writing:
```rust
struct IsAllowed;

impl Permission for IsAllowed {
fn call(&self, req: &HttpRequest, _payload: &mut Payload) -> Ready<actix_web::Result<bool>> {
todo!();
}
}
```

# Example
Dependencies:
```toml
[dependencies]
actix-permissions = "0.1.0-beta.1"
actix-permissions = "0.1.0"
```
Code:
```rust
Expand Down Expand Up @@ -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 <https://www.eisberg-labs.com/> (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
12 changes: 0 additions & 12 deletions example/Cargo.toml

This file was deleted.

10 changes: 10 additions & 0 deletions examples/hello-world/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
7 changes: 7 additions & 0 deletions examples/hello-world/README.md
Original file line number Diff line number Diff line change
@@ -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 <http://localhost:8080/>, then try <http://localhost:8080/?q>.
2 changes: 0 additions & 2 deletions example/src/main.rs → examples/hello-world/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DummyService>> = req.app_data::<Data<DummyService>>();
// TODO: do not unwrap here
ready(Ok(checker_service.unwrap().check(req.query_string())))
}

Expand All @@ -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<DummyService>> = req.app_data::<Data<DummyService>>();
// TODO: do not unwrap here
ready(Ok(checker_service.unwrap().check(req.query_string())))
}

Expand Down
18 changes: 18 additions & 0 deletions examples/role-based-authorization/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 12 additions & 0 deletions examples/role-based-authorization/README.md
Original file line number Diff line number Diff line change
@@ -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` <http://localhost:8080/admin>
- For Moderators and higher `moderator:2` <http://localhost:8080/mod>
- For Logged in users `user:3` <http://localhost:8080/>

# Running the App
```cargo run``` and go to <http://localhost:8080/>
49 changes: 49 additions & 0 deletions examples/role-based-authorization/src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -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<ServiceRequest, actix_web::Error> {
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
}
3 changes: 3 additions & 0 deletions examples/role-based-authorization/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod permissions;
pub mod models;
pub mod routes;
28 changes: 28 additions & 0 deletions examples/role-based-authorization/src/models.rs
Original file line number Diff line number Diff line change
@@ -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<User>{
vec![
User::new("admin", Role::Administrator, "1"),
User::new("moderator", Role::Moderator, "2"),
User::new("user", Role::User, "3"),
]
}
}
23 changes: 23 additions & 0 deletions examples/role-based-authorization/src/permissions.rs
Original file line number Diff line number Diff line change
@@ -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<actix_web::Result<bool>> {
let is_allowed = req.extensions().get::<Role>().map(|user_role| self.role >= *user_role).unwrap_or(false);
let res: actix_web::Result<bool, actix_web::Error> = 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 }
}
32 changes: 32 additions & 0 deletions examples/role-based-authorization/src/routes.rs
Original file line number Diff line number Diff line change
@@ -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<String, Error> {
Ok("Only for administrators!".to_string())
}

async fn moderators_index() -> Result<String, Error> {
Ok("Only for administrators and moderators!".to_string())
}

async fn index() -> Result<String, Error> {
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, ),
);
}
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
//!
//! #[actix_web::main]
//! async fn main() -> std::io::Result<()> {
//!
//!
//! HttpServer::new(|| {
//! App::new()
//! .service(web::scope("").route(
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod test_service;
41 changes: 41 additions & 0 deletions src/tests/test_service.rs
Original file line number Diff line number Diff line change
@@ -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<String, Error> {
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<actix_web::Result<bool, actix_web::Error>> {
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())
}
}
9 changes: 0 additions & 9 deletions tests/test_permission.rs

This file was deleted.

0 comments on commit de54163

Please sign in to comment.