5 minutes
GSoC: Week 7[During midterm Evaluation]
Duration
25th July - 31st July 2022
Tasks:
🟩 Axum real-world example:
- ✅ Define routes and api calls.
- ✅ Draft logic for auth.rs(middleware)
- ✅ Define structure of main.rs and enforce policies in backend
Outcomes
axum-middleware-example
api, middleware, routes
csv utils, main.rs, server setup
Workflow
Define routes and api calls
Following functions were to be implemented to make the api calls:
- signin() {}
- register() {}
- delete_user() {}
- get_user() {}
- get_all_user() {}
In this week, I was able to draft signin, get_user and get_all_user functions. In register and delete_user function I have to update the changes to both casbin-rules
and users
table in psql database.
Following is the code snippet for POST(api/auth/signin)
request:
api/user.rs
pub async fn signin(
Json(login_form): Json<LoginForm>,
pool: Extension<Pool>,
) -> Response {
// output should be a json response
match user::signin(login_form, &pool) {
Ok(token_res) => Json(ResponseBody::new(
constants::MESSAGE_SIGNIN_SUCCESS,
token_res,
))
.into_response(),
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
constants::MESSAGE_INTERNAL_SERVER_ERROR.to_string(),
)
.into_response(),
}
}
This matches the response with signin
functions defined under service/user.rs
. On failure, it returns the Error response as shown above. Unlike actix-web, where we get web::Json<Data>
, we have to use Json
extractor as shown above in axum.
service/user.rs
pub fn signin(
login: LoginForm,
Extension(pool): &Extension<Pool>,
) -> Result<TokenBodyResponse, ServiceError> {
match User::signin(login, &pool.get().unwrap()) {
Some(logged_user) => {
match serde_json::from_value(
json!({ "token": UserToken::generate_token(logged_user), "token_type": "bearer" }),
) {
Ok(token_res) => Ok(token_res),
Err(_) => Err(ServiceError::new(
StatusCode::INTERNAL_SERVER_ERROR,
constants::MESSAGE_INTERNAL_SERVER_ERROR.to_string(),
)),
}
}
None => Err(ServiceError::new(
StatusCode::INTERNAL_SERVER_ERROR,
constants::MESSAGE_SIGNIN_FAILED.to_string(),
)),
}
}
logged_user
is the LoginForm of the user, which contains user name, role, and login_session. For some logged user, we generate the token and in case there is no logged user, we simply return an error. In axum we need to extract the user data using Extension(T): Extension<T>
unlike actix, where it provides the web::Data(T)
.
model/user.rs
pub fn signin(login: LoginForm, conn: &Connection) -> Option<LoginInfo> {
if let Ok(user_to_verify) = users
.filter(email.eq(&login.email))
.get_result::<User>(conn)
{
if !user_to_verify.password.is_empty()
&& compare_password(&login.password, &user_to_verify.password).unwrap()
{
let login_session_str = User::generate_login_session();
if User::update_login_session_to_db(
&user_to_verify.email,
&login_session_str,
conn,
) {
return Some(LoginInfo {
username: user_to_verify.username,
role: user_to_verify.role,
login_session: login_session_str,
});
}
}
}
None
}
Above function implements the main logic to signin the user, it calls helper function from utils to compare the password from database and updates the login_session to the database.
Similar to the above, we have following api calls:
POST(api/auth/register)
DELETE(api/admin/{:id})
GET(api/user/{:id})
GET(api/users)
PUT(api/admin/{:id})
Draft logic for auth.rs(middleware)
let mut authenticate_pass: bool = false;
let mut authenticate_username: String = String::from("");
// Bypass account routes
let headers = req.headers_mut();
headers.append(
HeaderName::from_static("content-length"),
HeaderValue::from_static("true"),
);
if Method::OPTIONS == *req.method() {
authenticate_pass = true;
} else {
for ignore_route in constants::IGNORE_ROUTES.iter() {
if req.uri().path().starts_with(ignore_route) {
authenticate_pass = true;
break;
}
}
if !authenticate_pass {
if let Some(pool) = req.extensions().get::<Extension<Pool>>() {
info!("Connecting to database...");
if let Some(auth_header) =
req.headers().get(constants::AUTHORIZATION)
{
info!("Parsing authorization header...");
if let Ok(auth_str) = auth_header.to_str() {
if auth_str.starts_with("bearer")
|| auth_str.starts_with("Bearer")
{
info!("Parsing token...");
let token = auth_str[6..auth_str.len()].trim();
if let Ok(token_data) =
token_utils::decode_token(token.to_string())
{
info!("Decoding token...");
if token_utils::validate_token(&token_data, pool)
.is_ok()
{
info!("Valid token");
authenticate_username = token_data.claims.user_name;
authenticate_pass = true;
} else {
error!("Invalid token");
}
}
}
}
}
}
}
}
if authenticate_pass {
let vals = CasbinVals {
subject: authenticate_username,
domain: None,
};
req.extensions_mut().insert(vals);
Box::pin(async move {
Ok(inner.call(req).await?.map(body::boxed))
})
} else{
Box::pin(async move{
Ok(Json(ResponseBody::new(constants::MESSAGE_TOKEN_MISSING, constants::EMPTY)).into_response())
})
}
Above is the logic implemented for the auth.rs, which is supposed to respond to the request in this case. It basically bypass some account routes which are present in Constants.rs and does token validation(token is extracted from request headers) using validate_token
for the routes which couldn’t be bypassed.
Ignore routes:
pub const IGNORE_ROUTES: [&str; 4] = [
"api/auth/register",
"api/auth/signin",
"api/users",
"api/user",
];
Define structure of main.rs and enforce policies in backend
The basic skeleton of the main.rs should look like:
fn main() {
// EXTRACT DATA FROM .env FILE TO GET app_host, app_url, pool_size, app_port, app_database
// READ .csv FILE AND ADD/ENFORCE THE POLICIES IN casbin-rules TABLE
// IMPLEMENT CASBIN MIDDLEWARE
// SET ROUTES USING axum::Router
// HOST THE SERVER
}
Extract data from .env file
dotenv::dotenv().expect("Failed to read .env file, please add it");
std::env::set_var("RUST_LOG", "actix_web=debug");
env_logger::init();
let app_host = env::var("APP_HOST").expect("APP_HOST must be set.");
let app_port = env::var("APP_PORT").expect("APP_PORT must be set.");
let app_url = format!("{}:{}", &app_host, &app_port);
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let pool_size: u32 = std::env::var("POOL_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(8);
.env
file looks like
APP_HOST=127.0.0.1
APP_PORT=8080
DATABASE_URL=postgres://postgres:postgresAdmin@127.0.0.1:5432/casbintest
POOL_SIZE=8
HASH_ROUNDS=12
Enforce Policies
There are preset policies defined in .csv
file and they are needed to be added to the backend(using diesel adapter) before we start the operation of the application. We read them using the csv utils
as vector and add them to casbin-rules using enforcer.add_policy()
. It is important to classify the read vector into p
(policy) and g
(named_grouping_policy), hence a conditional is implemented which separates them.
Following code helps in understanding how policies are added to casbin-rules table
let preset_rules = load_csv(walk_csv("."));
for mut policy in preset_rules {
let ptype = policy.remove(0);
if ptype.starts_with('p') {
match clone_enforcer.write().await.add_policy(policy).await {
Ok(_) => info!("Present policies(p) added successfully"),
Err(err) => error!("Present policies(p) add error: {}", err.to_string()),
};
continue;
} else if ptype.starts_with('g') {
match clone_enforcer
.write()
.await
.add_named_grouping_policy(&ptype, policy)
.await
{
Ok(_) => info!("Preset policies(g) added successfully"),
Err(err) => error!("Preset policies(g) add error: {}", err.to_string()),
}
continue;
} else {
unreachable!()
}
}
Set Routes and host server
let app = Router::new()
.layer(Extension(pool.clone()))
.layer(Extension(clone_enforcer))
.route("/api/auth/register", post(user_api::register))
.route("/api/auth/signin", post(user_api::signin))
.route("/api/users", get(user_api::get_all_user))
.route("/api/user/:id", get(user_api::get_user))
.route("/api/admin/:id", put(user_api::update_user))
.route("/api/admin/:id", delete(user_api::delete_user))
.layer(casbin_middleware.clone())
.layer(middleware::auth::AuthLayer);
axum::Server::bind(&app_url.parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
Server is hosted at 127.0.0.1/8080
Next Week target
- Add remaining api commands, make clippy happy, resolve any present error.
- Start with casbin-openraft
918 Words
2022-07-30 01:08