Skip to content

Commit

Permalink
backend/db_connector: add authorization tokens model and migration
Browse files Browse the repository at this point in the history
  • Loading branch information
ffreddow committed Jan 7, 2025
1 parent a94cb17 commit e537ea4
Show file tree
Hide file tree
Showing 14 changed files with 468 additions and 6 deletions.
7 changes: 7 additions & 0 deletions backend/src/api_docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ async fn main() {
routes::user::update_password::update_password,
routes::user::update_user::update_user,
routes::user::get_secret::get_secret,
routes::user::create_authorization_token::create_authorization_token,
routes::user::get_authorization_tokens::get_authorization_tokens,
routes::user::delete::delete_user,
routes::management::management,
),
Expand All @@ -120,11 +122,16 @@ async fn main() {
routes::user::update_password::PasswordUpdateSchema,
routes::user::get_secret::GetSecretResponse,
routes::user::delete::DeleteUserSchema,
routes::user::create_authorization_token::CreateAuthorizationTokenResponseSchema,
routes::user::create_authorization_token::CreateAuthorizationTokenSchema,
routes::user::get_authorization_tokens::StrippedToken,
routes::user::get_authorization_tokens::GetAuthorizationTokensResponseSchema,
routes::management::ManagementSchema,
routes::management::ManagementResponseSchema,
routes::management::ManagementDataVersion,
routes::management::ManagementDataVersion1,
routes::management::ManagementDataVersion2,
routes::management::ConfiguredUser,
models::filtered_user::FilteredUser,
)),
modifiers(&JwtToken, &RefreshToken)
Expand Down
15 changes: 11 additions & 4 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ pub(crate) mod tests {
models::{recovery_tokens::RecoveryToken, refresh_tokens::RefreshToken, users::User},
test_connection_pool,
};
use diesel::r2d2::ConnectionManager;
use ipnetwork::Ipv4Network;
use lettre::transport::smtp::authentication::Credentials;
use lru::LruCache;
Expand Down Expand Up @@ -242,8 +243,8 @@ pub(crate) mod tests {
}
}

pub fn configure(cfg: &mut ServiceConfig) {
let pool = db_connector::test_connection_pool();
pub fn create_test_state(pool: Option<diesel::r2d2::Pool<ConnectionManager<PgConnection>>>) -> web::Data<AppState> {
let pool = pool.unwrap_or_else(|| db_connector::test_connection_pool());

let mail = std::env::var("MAIL_USER").expect("MAIL must be set");
let pass = std::env::var("MAIL_PASS").expect("MAIL_PASS must be set");
Expand All @@ -260,8 +261,14 @@ pub(crate) mod tests {
frontend_url: std::env::var("FRONTEND_URL").expect("FRONTEND_URL must be set!"),
};

web::Data::new(state)
}

pub fn configure(cfg: &mut ServiceConfig) {
let pool = db_connector::test_connection_pool();

let bridge_state = BridgeState {
pool,
pool: pool.clone(),
charger_management_map: Arc::new(Mutex::new(HashMap::new())),
charger_management_map_with_id: Arc::new(Mutex::new(HashMap::new())),
port_discovery: Arc::new(Mutex::new(HashMap::new())),
Expand All @@ -276,7 +283,7 @@ pub(crate) mod tests {
let cache: web::Data<Mutex<LruCache<String, Vec<u8>>>> =
web::Data::new(Mutex::new(LruCache::new(NonZeroUsize::new(10000).unwrap())));

let state = web::Data::new(state);
let state = create_test_state(Some(pool));
let bridge_state = web::Data::new(bridge_state);
let login_rate_limiter = web::Data::new(LoginRateLimiter::new());
let charger_rate_limiter = web::Data::new(ChargerRateLimiter::new());
Expand Down
2 changes: 2 additions & 0 deletions backend/src/routes/charger/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ pub async fn add(
let charger_id;

let (pub_key, password) =
// Updating a charger here is safe since we already had this combination of user and charger
// and the user_id is not fakable except someone stole our signing key for jwt.
if let Some(cid) = get_charger_uuid(&state, charger_uid, user_id.clone().into()).await? {
charger_id = cid;
update_charger(
Expand Down
40 changes: 39 additions & 1 deletion backend/src/routes/charger/allow_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use crate::{
charger::add::get_charger_from_db,
user::get_user_id,
},
utils::{get_connection, parse_uuid, web_block_unpacked},
utils::{get_connection, parse_uuid, validate_auth_token, web_block_unpacked},
AppState,
};

Expand All @@ -41,6 +41,7 @@ use super::add::{password_matches, Keys};
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
pub enum UserAuth {
LoginKey(String),
AuthToken(String),
}

#[derive(Debug, Deserialize, Serialize, ToSchema, Clone)]
Expand Down Expand Up @@ -107,6 +108,9 @@ async fn authenticate_user(
Err(_) => return Err(ErrorBadRequest("login_key is wrong base64")),
};
let _ = validate_password(&key, FindBy::Uuid(uid), conn).await?;
},
UserAuth::AuthToken(token) => {
validate_auth_token(token.to_owned(), uid, state).await?;
}
}
Ok(())
Expand Down Expand Up @@ -271,6 +275,40 @@ pub mod tests {
assert!(resp.status().is_success());
}

#[actix_web::test]
async fn test_allow_users_auth_token() {
let (mut user2, _) = TestUser::random().await;
let (mut user1, _) = TestUser::random().await;

let charger = OsRng.next_u32() as i32;
user1.login().await;
let charger = user1.add_charger(charger).await;

user2.login().await;
let auth_token = user2.create_authorization_token(true).await;

let app = App::new().configure(configure).service(allow_user);
let app = test::init_service(app).await;

let allow = AllowUserSchema {
charger_id: charger.uuid,
user_auth: UserAuth::AuthToken(auth_token),
email: user2.mail.to_owned(),
charger_password: charger.password,
wg_keys: generate_random_keys(),
charger_name: String::new(),
note: String::new(),
};
let req = test::TestRequest::put()
.uri("/allow_user")
.append_header(("X-Forwarded-For", "123.123.123.3"))
.set_json(allow)
.to_request();
let resp = test::call_service(&app, req).await;

assert!(resp.status().is_success());
}

#[actix_web::test]
async fn test_allow_wrong_credentials() {
let (user2, _) = TestUser::random().await;
Expand Down
117 changes: 117 additions & 0 deletions backend/src/routes/user/create_authorization_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use actix_web::{post, web, HttpResponse, Responder};
use base64::Engine;
use db_connector::models::authorization_tokens::AuthorizationToken;
use rand::RngCore;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use diesel::prelude::*;

use crate::{error::Error, utils::{get_connection, web_block_unpacked}, AppState};

#[derive(Serialize, Deserialize, ToSchema)]
pub struct CreateAuthorizationTokenResponseSchema {
token: String,
}

#[derive(Serialize, Deserialize, ToSchema)]
pub struct CreateAuthorizationTokenSchema {
use_once: bool,
}

#[utoipa::path(
context_path = "/user",
request_body = CreateAuthorizationTokenSchema,
responses(
(status = 201, body = CreateAuthorizationTokenResponseSchema),
),
security(
("jwt" = [])
)
)]
#[post("/create_authorization_token")]
pub async fn create_authorization_token(
state: web::Data<AppState>,
user_id: crate::models::uuid::Uuid,
schema: web::Json<CreateAuthorizationTokenSchema>,
) -> actix_web::Result<impl Responder>
{
let id = uuid::Uuid::new_v4();
let mut token = vec![0u8; 32];
rand::thread_rng().fill_bytes(&mut token);
let token = base64::engine::general_purpose::STANDARD.encode(token);
let auth_token = AuthorizationToken {
id,
user_id: user_id.clone().into(),
token: token.clone(),
use_once: schema.use_once,
};

let mut conn = get_connection(&state)?;
web_block_unpacked(move || {
use db_connector::schema::authorization_tokens::dsl as authorization_tokens;

match diesel::insert_into(authorization_tokens::authorization_tokens)
.values(&auth_token)
.execute(&mut conn) {
Ok(_) => Ok(()),
Err(_err) => Err(Error::InternalError)
}
}).await?;

let response = CreateAuthorizationTokenResponseSchema {
token,
};
Ok(HttpResponse::Created().json(response))
}

#[cfg(test)]
pub mod tests {
use actix_web::{cookie::Cookie, test::{self, TestRequest}, App};

use crate::{middleware::jwt::JwtMiddleware, routes::user::tests::TestUser, tests::configure};

use super::{create_authorization_token, CreateAuthorizationTokenResponseSchema, CreateAuthorizationTokenSchema};

pub async fn create_test_auth_token(user: &TestUser, use_once: bool) -> String {
let token = user.access_token.as_ref().unwrap();

let app = App::new().configure(configure)
.wrap(JwtMiddleware)
.service(create_authorization_token);
let app = test::init_service(app).await;

println!("{}", use_once);
let req = TestRequest::post()
.uri("/create_authorization_token")
.cookie(Cookie::new("access_token", token))
.set_json(CreateAuthorizationTokenSchema {
use_once
})
.to_request();

let resp: CreateAuthorizationTokenResponseSchema = test::call_and_read_body_json(&app, req).await;
resp.token
}

#[actix_web::test]
async fn test_authorization_token_creation() {
let (mut user, _) = TestUser::random().await;
let token = user.login().await;

let app = App::new().configure(configure)
.wrap(JwtMiddleware)
.service(create_authorization_token);
let app = test::init_service(app).await;

let req = TestRequest::post()
.uri("/create_authorization_token")
.cookie(Cookie::new("access_token", token))
.set_json(CreateAuthorizationTokenSchema {
use_once: true
})
.to_request();

let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 201);
}
}
97 changes: 97 additions & 0 deletions backend/src/routes/user/get_authorization_tokens.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use actix_web::{get, web, HttpResponse, Responder};
use db_connector::models::authorization_tokens::AuthorizationToken;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use crate::{error::Error, utils::{get_connection, web_block_unpacked}, AppState};

#[derive(Serialize, Deserialize, ToSchema)]
pub struct StrippedToken {
token: String,
use_once: bool,
}

#[derive(Serialize, Deserialize, ToSchema)]
pub struct GetAuthorizationTokensResponseSchema {
tokens: Vec<StrippedToken>,
}

#[utoipa::path(
context_path = "/user",
responses(
(status = 200, body = GetAuthorizationTokensResponseSchema),
),
security(
("jwt" = [])
)
)]
#[get("/get_authorization_tokens")]
pub async fn get_authorization_tokens(
state: web::Data<AppState>,
user_id: crate::models::uuid::Uuid,
) -> actix_web::Result<impl Responder> {

let mut conn = get_connection(&state)?;
let user_tokens: Vec<AuthorizationToken> = web_block_unpacked(move || {
use db_connector::schema::authorization_tokens::dsl as authorization_tokens;

let user_id: uuid::Uuid = user_id.into();
match authorization_tokens::authorization_tokens
.filter(authorization_tokens::user_id.eq(&user_id))
.select(AuthorizationToken::as_select())
.load(&mut conn)
{
Ok(u) => Ok(u),
Err(_err) => Err(Error::InternalError)
}
}).await?;

let tokens: Vec<StrippedToken> = user_tokens.into_iter().map(|t| {
StrippedToken {
token: t.token,
use_once: t.use_once,
}
}).collect();

Ok(HttpResponse::Ok().json(GetAuthorizationTokensResponseSchema {
tokens
}))
}

#[cfg(test)]
mod tests {
use actix_web::{cookie::Cookie, test, App};

use crate::{middleware::jwt::JwtMiddleware, routes::user::tests::TestUser, tests::configure};

use super::{get_authorization_tokens, GetAuthorizationTokensResponseSchema};


#[actix_web::test]
async fn test_get_authorization_tokens() {
let (mut user, _) = TestUser::random().await;
let access_token = user.login().await.to_string();

let mut auth_tokens = vec![String::new(); 5];
for token in auth_tokens.iter_mut() {
*token = user.create_authorization_token(true).await;
}

let app = App::new().configure(configure)
.wrap(JwtMiddleware)
.service(get_authorization_tokens);
let app = test::init_service(app).await;

let req = test::TestRequest::get()
.uri("/get_authorization_tokens")
.cookie(Cookie::new("access_token", access_token))
.to_request();

let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 200);
let resp: GetAuthorizationTokensResponseSchema = test::read_body_json(resp).await;
let resp_tokens: Vec<String> = resp.tokens.into_iter().map(|token| token.token).collect();
assert_eq!(auth_tokens, resp_tokens);
}
}
Loading

0 comments on commit e537ea4

Please sign in to comment.