diff --git a/config/server.toml b/config/server.toml index b7510a2056eb76b8a71056791afbd7597445ff03..64b6d040230ed742650095877ed1bc43f894adc2 100644 --- a/config/server.toml +++ b/config/server.toml @@ -21,7 +21,7 @@ redirect_url = "https://127.0.0.1:8080/api/v1/users/callback" [sign-backend] type = "memory" [memory.kms-provider] -type = "huaweicloud" +type = "dummy" kms_id = "65ccb4d8-cc45-4139-b380-2fcff184ac4f" endpoint = "cn-north-4.myhuaweicloud.com" project_name = "cn-north-4" diff --git a/migrations/20221220070859_initialize-table.up.sql b/migrations/20221220070859_initialize-table.up.sql index 1bed7870008c25ed95c5d2bcba166a21f0d18bc9..055b8904e8fe3cdd761cd0684bf67fef5e71b9ac 100644 --- a/migrations/20221220070859_initialize-table.up.sql +++ b/migrations/20221220070859_initialize-table.up.sql @@ -36,7 +36,9 @@ CREATE TABLE data_key ( CREATE TABLE token ( id INT AUTO_INCREMENT, user_id INT NOT NULL, + description VARCHAR(255), token VARCHAR(200), + create_at DATETIME, expire_at DATETIME, PRIMARY KEY(id), FOREIGN KEY (user_id) REFERENCES user(id) diff --git a/src/application/user.rs b/src/application/user.rs index d6dd392afd8391f15d619bc6b31c25781a513752..1fdc294afc5b4011e05740607c87cb2139192c4d 100644 --- a/src/application/user.rs +++ b/src/application/user.rs @@ -22,6 +22,7 @@ use crate::util::error::{Result, Error}; use async_trait::async_trait; use std::sync::Arc; use std::sync::RwLock; +use chrono::Utc; use serde::{Deserialize}; use config::Config; use reqwest::{header, Client}; @@ -33,15 +34,17 @@ use openidconnect::{ }; use openidconnect::{JsonWebKeySet, ClientId, AuthUrl, UserInfoUrl, TokenUrl, RedirectUrl, ClientSecret, IssuerUrl}; use url::Url; +use crate::presentation::handler::control::model::token::dto::TokenDTO; +use crate::util::key::{generate_api_token}; #[async_trait] pub trait UserService: Send + Sync{ async fn get_token(&self, u: &UserIdentity) -> Result>; - async fn get_token_by_value(&self, token: &str) -> Result; + async fn get_valid_token(&self, token: &str) -> Result; async fn save(&self, u: &User) -> Result; async fn get_user_by_id(&self, id: i32) -> Result; async fn get_by_email(&self, email: &str) -> Result; - async fn generate_token(&self, u: &UserIdentity) -> Result; + async fn generate_token(&self, u: &UserIdentity, token: TokenDTO) -> Result; async fn get_login_url(&self) -> Result; async fn validate_user(&self, code: &str) -> Result; } @@ -151,24 +154,36 @@ where R: UserRepository, T: TokenRepository { - async fn get_token_by_value(&self, token: &str) -> Result { - self.token_repository.get_token_by_value(token).await + async fn get_token(&self, user: &UserIdentity) -> Result> { + self.token_repository.get_token_by_user_id(user.id).await + } + + async fn get_valid_token(&self, token: &str) -> Result { + let token = self.token_repository.get_token_by_value(token).await?; + if token.expire_at.gt(&Utc::now()) { + return Ok(token) + } + Err(Error::TokenExpiredError(token.to_string())) } async fn save(&self, u: &User) -> Result { return self.user_repository.create(u).await } - async fn get_token(&self, user: &UserIdentity) -> Result> { - self.token_repository.get_token_by_user_id(user.id).await + async fn get_user_by_id(&self, id: i32) -> Result { + self.user_repository.get_by_id(id).await } async fn get_by_email(&self, email: &str) -> Result { self.user_repository.get_by_email(email).await } - async fn generate_token(&self, u: &UserIdentity) -> Result { - return self.token_repository.create(&Token::new(u.id)?).await + async fn generate_token(&self, u: &UserIdentity, token: TokenDTO) -> Result { + let real_token = generate_api_token(); + let created = Token::new(token.id, u.id, token.description, real_token)?; + self.token_repository.create(&created).await?; + Ok(created) + } async fn get_login_url(&self) -> Result { @@ -193,8 +208,4 @@ where } } } - - async fn get_user_by_id(&self, id: i32) -> Result { - self.user_repository.get_by_id(id).await - } } diff --git a/src/domain/token/entity.rs b/src/domain/token/entity.rs index 232aad5b1c5eb1412dba1f368f86ccbc1739d508..24548fe83cccd579d1eae3a6bc9a66154840d5b4 100644 --- a/src/domain/token/entity.rs +++ b/src/domain/token/entity.rs @@ -19,16 +19,15 @@ use crate::util::error::Result; use chrono::{DateTime, Duration, Utc}; use std::fmt::{Display, Formatter}; - -use crate::util::key::generate_api_token; - const TOKEN_EXPIRE_IN_DAYS: i64 = 180; #[derive(Debug)] pub struct Token { pub id: i32, pub user_id: i32, + pub description: String, pub token: String, + pub create_at: DateTime, pub expire_at: DateTime, } @@ -43,12 +42,14 @@ impl Display for Token { } impl Token { - pub fn new(user_id: i32) -> Result { + pub fn new(id: i32, user_id: i32, description: String, token: String) -> Result { let now = Utc::now(); Ok(Token { - id: 0, + id, user_id, - token: generate_api_token(), + description, + token, + create_at: now, expire_at: now + Duration::days(TOKEN_EXPIRE_IN_DAYS), }) } diff --git a/src/infra/database/model/token/dto.rs b/src/infra/database/model/token/dto.rs index 2c4b0db76cc49cdba12daf66fb91cbb9530ce84c..1bb74657aadf2bf3ab414824a850b9fdd5707811 100644 --- a/src/infra/database/model/token/dto.rs +++ b/src/infra/database/model/token/dto.rs @@ -20,12 +20,15 @@ use sqlx::FromRow; use chrono::{DateTime, Utc}; use crate::domain::token::entity::Token; +use crate::util::key::get_token_hash; #[derive(Debug, FromRow)] pub(super) struct TokenDTO { pub id: i32, pub user_id: i32, + pub description: String, pub token: String, + pub create_at: DateTime, pub expire_at: DateTime, } @@ -35,7 +38,9 @@ impl TokenDTO { Ok(Self { id: token.id, user_id: token.user_id, - token: token.token.clone(), + description: token.description.clone(), + token: get_token_hash(&token.token), + create_at: token.create_at, expire_at: token.expire_at, }) } @@ -43,7 +48,9 @@ impl TokenDTO { Ok(Token { id: self.id, user_id: self.user_id, + description: self.description.clone(), token: self.token.clone(), + create_at: self.create_at, expire_at:self.expire_at, }) } diff --git a/src/infra/database/model/token/repository.rs b/src/infra/database/model/token/repository.rs index 300e6655f10f1fc839fac476039a246752d7fd15..145e1299a7d8e934a1732dff12b38b85bab98070 100644 --- a/src/infra/database/model/token/repository.rs +++ b/src/infra/database/model/token/repository.rs @@ -22,6 +22,7 @@ use async_trait::async_trait; use std::boxed::Box; use crate::infra::database::model::token::dto::TokenDTO; +use crate::util::key::get_token_hash; #[derive(Clone)] @@ -42,9 +43,11 @@ impl Repository for TokenRepository { async fn create(&self, token: &Token) -> Result { let dto = TokenDTO::encrypt(token).await?; - let record : u64 = sqlx::query("INSERT INTO token(user_id, token, expire_at) VALUES (?, ?, ?)") + let record : u64 = sqlx::query("INSERT INTO token(user_id, description, token, create_at, expire_at) VALUES (?, ?, ?, ?, ?)") .bind(&dto.user_id) + .bind(&dto.description) .bind(&dto.token) + .bind(&dto.create_at) .bind(&dto.expire_at) .execute(&self.db_pool) .await?.last_insert_id(); @@ -61,7 +64,7 @@ impl Repository for TokenRepository { async fn get_token_by_value(&self, token: &str) -> Result { let selected: TokenDTO = sqlx::query_as("SELECT * FROM token WHERE token = ?") - .bind(token) + .bind(get_token_hash(token)) .fetch_one(&self.db_pool) .await?; Ok(selected.decrypt().await?) diff --git a/src/infra/sign_plugin/openpgp.rs b/src/infra/sign_plugin/openpgp.rs index e3fd9690e621ed284f1d553eaf6af1f5168eb9ef..33f480428e84921e0f1b7dcf8cdc9abf49d061e7 100644 --- a/src/infra/sign_plugin/openpgp.rs +++ b/src/infra/sign_plugin/openpgp.rs @@ -55,7 +55,7 @@ pub struct PgpKeyGenerationParameter { key_length: String, #[validate(custom(function= "validate_digest_algorithm_type", message="invalid digest algorithm"))] digest_algorithm: String, - #[validate(custom(function = "validate_utc_time", message="invalid openpgp attribute 'created_at'"))] + #[validate(custom(function = "validate_utc_time", message="invalid openpgp attribute 'create_at'"))] create_at: String, #[validate(custom(function= "validate_utc_time_not_expire", message="invalid openpgp attribute 'expire_at'"))] expire_at: String, diff --git a/src/infra/sign_plugin/x509.rs b/src/infra/sign_plugin/x509.rs index 48d0545e3009f09c2280a6fe61880925700fbb82..de1fae65c2ad9216d28cd8ab229fc3fe29fb533e 100644 --- a/src/infra/sign_plugin/x509.rs +++ b/src/infra/sign_plugin/x509.rs @@ -53,7 +53,7 @@ pub struct X509KeyGenerationParameter { key_length: String, #[validate(custom(function= "validate_x509_digest_algorithm_type", message="invalid digest algorithm"))] digest_algorithm: String, - #[validate(custom(function = "validate_utc_time", message="invalid x509 attribute 'created_at'"))] + #[validate(custom(function = "validate_utc_time", message="invalid x509 attribute 'create_at'"))] create_at: String, #[validate(custom(function= "validate_utc_time_not_expire", message="invalid x509 attribute 'expire_at'"))] expire_at: String, diff --git a/src/presentation/handler/control/model/token/dto.rs b/src/presentation/handler/control/model/token/dto.rs index 2801710e88d9fed366c956ecfc95f7f1ad9535f6..0439631fa9c7a56a0dc5bbf64fffddce4e57615e 100644 --- a/src/presentation/handler/control/model/token/dto.rs +++ b/src/presentation/handler/control/model/token/dto.rs @@ -10,18 +10,43 @@ use crate::domain::token::entity::Token; #[derive(Debug, Deserialize, Serialize)] pub struct TokenDTO { + #[serde(skip_deserializing)] + pub id: i32, + #[serde(skip_deserializing)] + pub user_id: i32, + #[serde(skip_deserializing)] pub token: String, + pub description: String, + #[serde(skip_deserializing)] + pub create_at: DateTime, + #[serde(skip_deserializing)] pub expire_at: DateTime } +impl TokenDTO { + pub fn new(description: String) -> TokenDTO { + TokenDTO { + id: 0, + user_id: 0, + //disable parse hash to dto + token: "".to_string(), + description, + expire_at: Default::default(), + create_at: Default::default(), + } + } +} + impl From for Token { fn from(token: TokenDTO) -> Self { Token { id: 0, user_id: 0, - token: token.token.clone(), - expire_at: token.expire_at, + description: token.description.clone(), + token: Default::default(), + create_at: Default::default(), + expire_at: Default::default(), } } } @@ -29,8 +54,12 @@ impl From for Token { impl From for TokenDTO { fn from(token: Token) -> Self { TokenDTO { + id: token.id, + user_id: token.user_id, token: token.token.clone(), + description: token.description.clone(), expire_at: token.expire_at, + create_at: token.create_at, } } } \ No newline at end of file diff --git a/src/presentation/handler/control/model/user/dto.rs b/src/presentation/handler/control/model/user/dto.rs index 06b7f0abe685a27b21ff74ff3f2b39f3e212286b..952efcb09be38788f637f51b5ee5ec8fb74bee08 100644 --- a/src/presentation/handler/control/model/user/dto.rs +++ b/src/presentation/handler/control/model/user/dto.rs @@ -7,7 +7,6 @@ use std::pin::Pin; use futures::Future; use serde::{Deserialize, Serialize}; use std::convert::From; -use chrono::{Utc}; use crate::application::user::UserService; use crate::domain::user::entity::User; @@ -38,14 +37,9 @@ impl FromRequest for UserIdentity { None => { if let Some(value) = req.clone().headers().get("Authorization") { if let Some(user_service) = req.clone().app_data::>() { - if let Ok(token) = user_service.get_ref().get_token_by_value(value.to_str().unwrap()).await { - //token exists and valid - if token.expire_at.gt(&Utc::now()) { - if let Ok(user) = user_service.get_ref().get_user_by_id(token.user_id).await { - return Ok(UserIdentity::from(user)); - } - } else { - warn!("token expired"); + if let Ok(token) = user_service.get_ref().get_valid_token(value.to_str().unwrap()).await { + if let Ok(user) = user_service.get_ref().get_user_by_id(token.user_id).await { + return Ok(UserIdentity::from(user)); } } else { warn!("unable to find token record"); diff --git a/src/presentation/handler/control/user_handler.rs b/src/presentation/handler/control/user_handler.rs index 82f4ac0643d4c3bab9e6998cd5e1095139a29f3e..133af8f81892ed5b17c70022d7de37362217e523 100644 --- a/src/presentation/handler/control/user_handler.rs +++ b/src/presentation/handler/control/user_handler.rs @@ -53,8 +53,8 @@ async fn callback(req: HttpRequest, user_service: web::Data, co } } -async fn new_token(user: UserIdentity, user_service: web::Data) -> Result { - let token = user_service.into_inner().generate_token(&user).await?; +async fn new_token(user: UserIdentity, user_service: web::Data, token: web::Json) -> Result { + let token = user_service.into_inner().generate_token(&user, token.0).await?; Ok(HttpResponse::Ok().json(TokenDTO::from(token))) } @@ -73,5 +73,5 @@ pub fn get_scope() -> Scope { .service(web::resource("/login").route(web::get().to(login))) .service(web::resource("/logout").route(web::post().to(logout))) .service(web::resource("/callback").route(web::get().to(callback))) - .service(web::resource("/api_keys").route(web::get().to(new_token)).route(web::post().to(list_token))) + .service(web::resource("/api_keys").route(web::post().to(new_token)).route(web::get().to(list_token))) } \ No newline at end of file diff --git a/src/presentation/server/control_server.rs b/src/presentation/server/control_server.rs index 1949988c56feb3a23681d32dd74197251f980641..fda1c06c8ce8dbec7cdc32d3107b2a00038e5c4b 100644 --- a/src/presentation/server/control_server.rs +++ b/src/presentation/server/control_server.rs @@ -39,6 +39,7 @@ use crate::application::user::{DBUserService, UserService}; use crate::domain::datakey::entity::DataKey; use crate::domain::token::entity::Token; use crate::domain::user::entity::User; +use crate::presentation::handler::control::model::token::dto::TokenDTO; use crate::presentation::handler::control::model::user::dto::UserIdentity; pub struct ControlServer { @@ -171,7 +172,9 @@ impl ControlServer { //used for control admin cmd pub async fn create_user_token(&self, user: &User) -> Result { let user = self.user_service.save(user).await?; - self.user_service.generate_token(&UserIdentity::from(user)).await + self.user_service.generate_token( + &UserIdentity::from(user.clone()), + TokenDTO::new("default admin token".to_owned())).await } //used for control admin cmd diff --git a/src/util/error.rs b/src/util/error.rs index 9cf217aa8f080b12f22e870ce75768bc0b12787f..f219fb74517ca788ac57de8ffb36f6daa9c5579d 100644 --- a/src/util/error.rs +++ b/src/util/error.rs @@ -86,6 +86,8 @@ pub enum Error { AuthError(String), #[error("failed to connect to redis store: {0}")] RedisError(String), + #[error("token has expired: {0}")] + TokenExpiredError(String), //client error #[error("file type not supported {0}")] diff --git a/src/util/key.rs b/src/util/key.rs index 2a28008694dd2cee0b9a5c59faf32024bf6960f7..d183305eab85db1c39eed50797b45fcb9a6aeed5 100644 --- a/src/util/key.rs +++ b/src/util/key.rs @@ -19,6 +19,7 @@ use rand::{thread_rng, Rng}; use rand::distributions::Alphanumeric; use serde::{Serialize, Serializer}; use std::collections::{HashMap, BTreeMap}; +use sha1::Digest; pub fn encode_u8_to_hex_string(value: &[u8]) -> String { value @@ -35,6 +36,13 @@ pub fn generate_api_token() -> String { thread_rng().sample_iter(&Alphanumeric).take(40).map(char::from).collect() } +pub fn get_token_hash(real_token: &str) -> String { + let mut hasher = sha1::Sha1::default(); + hasher.update(real_token); + let digest = hasher.finalize(); + return hex::encode(digest) +} + pub fn sorted_map(value: &HashMap, serializer: S) -> Result { let mut items: Vec<(_, _)> = value.iter().collect(); items.sort_by(|a, b| a.0.cmp(&b.0));