Skip to content

Commit

Permalink
Refactor auth
Browse files Browse the repository at this point in the history
  • Loading branch information
bubelov committed Sep 30, 2024
1 parent 130c1f4 commit 491a44c
Show file tree
Hide file tree
Showing 30 changed files with 190 additions and 215 deletions.
23 changes: 0 additions & 23 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 1 addition & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,4 @@ deadpool-sqlite = { version = "0.8.1", default-features = false, features = ["rt
actix-governor = { version = "0.5.0", default-features = false }

# https://crates.io/crates/jsonrpc-v2
jsonrpc-v2 = { version = "0.13.0", default-features = false, features = ["actix-web-v4-integration", "easy-errors"] }

[dependencies.uuid]
version = "1.10.0"
features = [
"v4", # Lets you generate random UUIDs
"fast-rng", # Use a faster (but still sufficiently random) RNG
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
]
jsonrpc-v2 = { version = "0.13.0", default-features = false, features = ["actix-web-v4-integration", "easy-errors"] }
12 changes: 12 additions & 0 deletions migrations/62.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
DROP TRIGGER token_updated_at;

ALTER TABLE token RENAME TO admin;

ALTER TABLE admin RENAME COLUMN owner TO name;

ALTER TABLE admin RENAME COLUMN secret TO password;

CREATE TRIGGER admin_updated_at UPDATE OF name, password, allowed_methods, created_at, deleted_at ON admin
BEGIN
UPDATE admin SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') WHERE id = old.id;
END;
2 changes: 1 addition & 1 deletion src/auth/mod.rs → src/admin/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
pub mod model;
pub use model::Token;
pub use model::Admin;
pub mod service;
66 changes: 30 additions & 36 deletions src/auth/model.rs → src/admin/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,36 @@ use std::thread::sleep;
use std::time::Duration;

#[allow(dead_code)]
pub struct Token {
pub struct Admin {
pub id: i64,
pub owner: String,
pub secret: String,
pub name: String,
pub password: String,
pub allowed_methods: Vec<String>,
pub created_at: String,
pub updated_at: String,
pub deleted_at: Option<String>,
}

const TABLE: &str = "token";
const ALL_COLUMNS: &str = "id, owner, secret, allowed_methods, created_at, updated_at, deleted_at";
const TABLE: &str = "admin";
const ALL_COLUMNS: &str = "id, name, password, allowed_methods, created_at, updated_at, deleted_at";
const _COL_ID: &str = "id";
const COL_OWNER: &str = "owner";
const COL_SECRET: &str = "secret";
const COL_ALLOWED_METHODS: &str = "allowed_methods";
const COL_NAME: &str = "name";
const COL_PASSWORD: &str = "password";
const _COL_ALLOWED_METHODS: &str = "allowed_methods";
const _COL_CREATED_AT: &str = "created_at";
const _COL_UPDATED_AT: &str = "updated_at";
const _COL_DELETED_AT: &str = "deleted_at";

impl Token {
pub fn insert(
owner: &str,
secret: &str,
allowed_methods: Vec<String>,
conn: &Connection,
) -> Result<Option<Token>> {
let allowed_methods =
Value::Array(allowed_methods.into_iter().map(|it| it.into()).collect());
impl Admin {
pub fn insert(name: &str, password: &str, conn: &Connection) -> Result<Option<Admin>> {
let query = format!(
r#"
INSERT INTO token (
{COL_OWNER},
{COL_SECRET},
{COL_ALLOWED_METHODS}
INSERT INTO {TABLE} (
{COL_NAME},
{COL_PASSWORD}
) VALUES (
:owner,
:secret,
:allowed_methods
:name,
:password
)
"#
);
Expand All @@ -54,35 +45,38 @@ impl Token {
conn.execute(
&query,
named_params! {
":owner": owner,
":secret": secret,
":allowed_methods": allowed_methods,
":name": name,
":password": password,
},
)?;
Ok(Token::select_by_secret(secret, conn)?)
Ok(Admin::select_by_password(password, conn)?)
}

pub fn select_by_secret(secret: &str, conn: &Connection) -> Result<Option<Token>> {
pub fn select_by_password(password: &str, conn: &Connection) -> Result<Option<Admin>> {
let query = format!(
r#"
SELECT {ALL_COLUMNS}
FROM {TABLE}
WHERE {COL_SECRET} = :secret
WHERE {COL_PASSWORD} = :password
"#
);
let res = conn
.query_row(&query, named_params! { ":secret": secret }, Self::mapper())
.query_row(
&query,
named_params! { ":password": password },
Self::mapper(),
)
.optional()?;
Ok(res)
}

const fn mapper() -> fn(&Row) -> rusqlite::Result<Token> {
|row: &Row| -> rusqlite::Result<Token> {
const fn mapper() -> fn(&Row) -> rusqlite::Result<Admin> {
|row: &Row| -> rusqlite::Result<Admin> {
let allowed_methods: Value = row.get(3)?;
Ok(Token {
Ok(Admin {
id: row.get(0)?,
owner: row.get(1)?,
secret: row.get(2)?,
name: row.get(1)?,
password: row.get(2)?,
allowed_methods: allowed_methods
.as_array()
.unwrap()
Expand Down
31 changes: 16 additions & 15 deletions src/auth/service.rs → src/admin/service.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
use super::Token;
use super::Admin;
use crate::Result;
use crate::{discord, Error};
use actix_web::{http::header::HeaderMap, HttpRequest};
use deadpool_sqlite::Pool;
use rusqlite::Connection;
use tracing::warn;

#[cfg(test)]
pub async fn mock_token(secret: &str, pool: &Pool) -> Token {
let secret = secret.to_string();
pub async fn mock_admin(password: &str, pool: &Pool) -> Admin {
let password = password.to_string();
pool.get()
.await
.unwrap()
.interact(move |conn| Token::insert("test", &secret, vec![], conn))
.interact(move |conn| Admin::insert("test", &password, conn))
.await
.unwrap()
.unwrap()
.unwrap()
}

pub async fn check(req: &HttpRequest, pool: &Pool) -> Result<Token, Error> {
pub async fn check(req: &HttpRequest, pool: &Pool) -> Result<Admin> {
let headers = req.headers().clone();
let guard = pool.get().await.unwrap();
let conn = guard.lock().unwrap();
get_admin_token(&conn, &headers).await
get_admin(&conn, &headers).await
}

pub async fn get_admin_token(db: &Connection, headers: &HeaderMap) -> Result<Token, Error> {
pub async fn get_admin(db: &Connection, headers: &HeaderMap) -> Result<Admin> {
let auth_header = headers
.get("Authorization")
.map(|it| it.to_str().unwrap_or(""))
Expand All @@ -41,17 +42,17 @@ pub async fn get_admin_token(db: &Connection, headers: &HeaderMap) -> Result<Tok
"Authorization header is invalid".into(),
))?
}
let secret = auth_header_parts[1];
let token = Token::select_by_secret(secret, db)?;
match token {
Some(token) => {
return Ok(token);
let password = auth_header_parts[1];
let admin = Admin::select_by_password(password, db)?;
match admin {
Some(admin) => {
return Ok(admin);
}
None => {
let log_message = "Someone tried and failed to access admin API";
warn!(log_message);
discord::send_message_to_channel(log_message, discord::CHANNEL_API).await;
Err(Error::HttpUnauthorized("Invalid token".into()))?
Err(Error::HttpUnauthorized("Invalid bearer token".into()))?
}
}
}
Expand All @@ -74,7 +75,7 @@ mod tests {
#[actix_web::test]
async fn no_header() -> Result<()> {
let state = mock_state().await;
super::mock_token("test", &state.pool).await.secret;
super::mock_admin("test", &state.pool).await;
let app = test::init_service(
App::new()
.app_data(Data::new(state.pool))
Expand All @@ -90,7 +91,7 @@ mod tests {
#[actix_web::test]
async fn valid_token() -> Result<()> {
let state = mock_state().await;
super::mock_token("test", &state.pool).await.secret;
super::mock_admin("test", &state.pool).await;
let app = test::init_service(
App::new()
.app_data(Data::new(state.pool))
Expand Down
24 changes: 14 additions & 10 deletions src/area/admin.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
admin::{self},
area::{self, Area},
auth::{self},
discord, Error,
};
use actix_web::{
Expand Down Expand Up @@ -46,7 +46,7 @@ pub async fn patch(
args: Json<PatchArgs>,
pool: Data<Arc<Pool>>,
) -> Result<Json<AreaView>, Error> {
let token = auth::service::check(&req, &pool).await?;
let admin = admin::service::check(&req, &pool).await?;
let cloned_id_or_alias = id_or_alias.clone();
let area = pool
.get()
Expand All @@ -66,7 +66,7 @@ pub async fn patch(
.await??;
let log_message = format!(
"{} updated area https://api.btcmap.org/v3/areas/{}",
token.owner, area.id,
admin.name, area.id,
);
warn!(log_message);
discord::send_message_to_channel(&log_message, discord::CHANNEL_API).await;
Expand All @@ -79,7 +79,7 @@ pub async fn delete(
id_or_alias: Path<String>,
pool: Data<Arc<Pool>>,
) -> Result<Json<AreaView>, Error> {
let token = auth::service::check(&req, &pool).await?;
let admin = admin::service::check(&req, &pool).await?;
let cloned_id_or_alias = id_or_alias.clone();
let area = pool
.get()
Expand All @@ -97,7 +97,7 @@ pub async fn delete(
.await??;
let log_message = format!(
"{} deleted area https://api.btcmap.org/v3/areas/{}",
token.owner, area.id,
admin.name, area.id,
);
warn!(log_message);
Ok(area.into())
Expand Down Expand Up @@ -128,7 +128,7 @@ mod test {
use crate::element::Element;
use crate::osm::overpass::OverpassElement;
use crate::test::{mock_state, phuket_geo_json};
use crate::{auth, Result};
use crate::{admin, Result};
use actix_web::http::StatusCode;
use actix_web::test::TestRequest;
use actix_web::web::Data;
Expand Down Expand Up @@ -162,7 +162,9 @@ mod test {
#[test]
async fn patch_should_update_area() -> Result<()> {
let state = mock_state().await;
let token = auth::service::mock_token("test", &state.pool).await.secret;
let admin_password = admin::service::mock_admin("test", &state.pool)
.await
.password;
let url_alias = "test";
let mut tags = Map::new();
tags.insert("url_alias".into(), Value::String(url_alias.into()));
Expand All @@ -186,7 +188,7 @@ mod test {
let args: Value = serde_json::from_str(args)?;
let req = TestRequest::patch()
.uri(&format!("/{url_alias}"))
.append_header(("Authorization", format!("Bearer {token}")))
.append_header(("Authorization", format!("Bearer {admin_password}")))
.set_json(args)
.to_request();
let res = test::call_service(&app, req).await;
Expand Down Expand Up @@ -224,7 +226,9 @@ mod test {
async fn delete_should_soft_delete_area() -> Result<()> {
let state = mock_state().await;

let token = auth::service::mock_token("test", &state.pool).await.secret;
let admin_password = admin::service::mock_admin("test", &state.pool)
.await
.password;

let url_alias = "test";
let mut tags = Map::new();
Expand Down Expand Up @@ -269,7 +273,7 @@ mod test {
.await;
let req = TestRequest::delete()
.uri(&format!("/{url_alias}"))
.append_header(("Authorization", format!("Bearer {token}")))
.append_header(("Authorization", format!("Bearer {admin_password}")))
.to_request();
let res = test::call_service(&app, req).await;
assert_eq!(res.status(), StatusCode::OK);
Expand Down
Loading

0 comments on commit 491a44c

Please sign in to comment.