Add agents

This commit is contained in:
Adrian Woźniak 2023-12-28 21:16:32 +01:00
parent 765c8753dd
commit e054860091
9 changed files with 365 additions and 0 deletions

View File

@ -0,0 +1,10 @@
pub use database::sea_orm_migration::prelude::*;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![]
}
}

View File

@ -0,0 +1,12 @@
[package]
name = "sessions-agent"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
tokio = { version = "1.27.0", features = ["full"] }
database = { workspace = true }
async-trait = "0.1.75"
events = { workspace = true }
actix = "0.13.1"

View File

@ -0,0 +1,3 @@
#[actix::main]
async fn main() {
}

View File

@ -0,0 +1,10 @@
pub use database::sea_orm_migration::prelude::*;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![]
}
}

View File

@ -0,0 +1,294 @@
use std::collections::HashSet;
use std::hash::{DefaultHasher, Hasher};
use std::num::TryFromIntError;
use std::path::PathBuf;
use std::sync::Arc;
use chrono::NaiveDateTime;
use database::chrono::Utc;
use database::sea_orm::ActiveValue::*;
use database::{chrono, sessions, uuid};
use jsonwebtoken::*;
use serde::{Deserialize, Serialize};
use tracing::*;
pub static KEYS_PATH: &str = "./config";
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
#[serde(rename = "sum")]
pub hash_sum: i64,
#[serde(rename = "sub")]
pub subject: i32,
#[serde(rename = "aud")]
pub audience: String,
pub role: String,
#[serde(rename = "iss")]
pub issuer: String,
#[serde(rename = "nbt")]
pub not_before_time: NaiveDateTime,
#[serde(rename = "exp")]
pub exp: NaiveDateTime,
#[serde(rename = "iat")]
pub issued_at: NaiveDateTime,
#[serde(rename = "jti")]
pub jwt_unique_identifier: uuid::Uuid,
}
impl Claims {
pub async fn new(
subject: i32,
audience: String,
role: String,
issuer: Option<String>,
not_before_time: chrono::NaiveDateTime,
issued_at: chrono::NaiveDateTime,
expiration_duration: Option<chrono::Duration>,
) -> Result<Self, std::num::TryFromIntError> {
let issuer = issuer.unwrap_or_else(|| "ergokeyboard".to_owned());
let mut claims = Claims {
subject,
audience,
role,
issuer,
hash_sum: 0,
not_before_time,
exp: issued_at + expiration_duration.unwrap_or(chrono::Duration::days(365)),
issued_at,
jwt_unique_identifier: uuid::Uuid::new_v4(),
};
let hash_sum = generate_hash_sum(&claims).await as i64;
claims.hash_sum = hash_sum;
Ok(claims)
}
}
macro_rules! cmp_both {
($l: expr, $r: expr, $($field: ident),+) => {
$(
$l.$field == $r.$field &&
)+ true
}
}
impl PartialEq<sessions::Model> for Claims {
fn eq(&self, s: &sessions::Model) -> bool {
cmp_both!(
self,
s,
hash_sum,
subject,
audience,
role,
issuer,
not_before_time,
issued_at,
jwt_unique_identifier
) && self.exp == s.expiration_time
}
}
#[derive(Debug, thiserror::Error)]
pub enum KeysError {
#[error("Decode key failed on file system error: {0}")]
DecodeKeyIo(std::io::Error),
#[error("Decode key failed on file system error: {0}")]
EncodeKeyIo(std::io::Error),
#[error("Decode key failed to parse ed25519 key: {0}")]
DecodeKeyParsing(jsonwebtoken::errors::Error),
#[error("Encode key failed to parse ed25519 key: {0}")]
EncodeKeyParsing(jsonwebtoken::errors::Error),
}
pub struct JwtKeysInner {
decode: DecodingKey,
encode: EncodingKey,
}
const DECODE_KEY_NAME: &str = "public.pem";
const ENCODE_KEY_NAME: &str = "private.pem";
#[derive(Clone, derive_more::Deref)]
pub struct JwtKeys(Arc<JwtKeysInner>);
impl JwtKeys {
pub fn load(config_path: PathBuf) -> Result<Self, KeysError> {
Ok(Self(Arc::new(JwtKeysInner {
decode: DecodingKey::from_ed_pem(
&std::fs::read(config_path.join(DECODE_KEY_NAME))
.map_err(KeysError::DecodeKeyIo)?,
)
.map_err(KeysError::DecodeKeyParsing)?,
encode: EncodingKey::from_ed_pem(
&std::fs::read(config_path.join(ENCODE_KEY_NAME))
.map_err(KeysError::EncodeKeyIo)?,
)
.map_err(KeysError::EncodeKeyParsing)?,
})))
}
}
pub async fn generate_token(
subject: i32,
audience: String,
role: String,
issuer: Option<String>,
not_before_time: chrono::NaiveDateTime,
issued_at: chrono::NaiveDateTime,
expiration_duration: Option<chrono::Duration>,
) -> Result<database::sessions::ActiveModel, TryFromIntError> {
let claims = Claims::new(
subject,
audience,
role,
issuer.clone(),
not_before_time,
issued_at,
expiration_duration,
)
.await?;
Ok(database::sessions::ActiveModel {
hash_sum: Set(claims.hash_sum),
subject: Set(claims.subject),
audience: Set(claims.audience),
role: Set(claims.role),
issuer: Set(claims.issuer),
not_before_time: Set(claims.not_before_time),
expiration_time: Set(claims.exp),
issued_at: Set(claims.issued_at),
jwt_unique_identifier: Set(claims.jwt_unique_identifier),
..Default::default()
})
}
pub async fn generate_hash_sum(claims: &Claims) -> i64 {
let Claims {
subject,
hash_sum: _,
audience,
role,
issuer,
not_before_time,
exp: expiration_time,
issued_at,
jwt_unique_identifier,
} = claims;
let mut hasher = DefaultHasher::default();
hasher.write_i32(*subject);
hasher.write(audience.as_bytes());
hasher.write(role.as_bytes());
hasher.write(issuer.as_bytes());
hasher.write_i64(not_before_time.timestamp_nanos_opt().expect("invalid NBT"));
hasher.write_i64(expiration_time.timestamp_nanos_opt().expect("invalid EXP"));
hasher.write_i64(issued_at.timestamp_nanos_opt().expect("invalid IAT"));
hasher.write(jwt_unique_identifier.as_bytes());
hasher.finish() as i64
}
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
#[error("Given JWT text is not valid")]
InvalidString,
#[error("Can't load accounts from database")]
FetchAccounts,
#[error("Can't load sessions from database")]
FetchSessions,
#[error("Account for given ID does not exists")]
NoAccount,
#[error("Given token does not exists")]
UnknownToken,
#[error("Given token expired")]
Expired,
}
pub async fn validate(
db_client: database::DatabaseConnection,
keys: JwtKeys,
token: &str,
) -> Result<database::accounts::Model, ValidationError> {
use database::*;
let mut validation = jsonwebtoken::Validation::new(Algorithm::EdDSA);
validation.validate_exp = false;
validation.required_spec_claims = HashSet::new();
validation.set_audience(&["Web"]);
validation.set_issuer(&["ergokeyboard"]);
tracing::info!("decoding token: {token:?}");
let token = match jsonwebtoken::decode::<Claims>(
&token,
&keys.decode,
&validation,
) {
Err(e) => {
warn!("Failed to decode token: {e}");
return Err(ValidationError::InvalidString);
}
Ok(token) => token.claims,
};
tracing::trace!("claims are: {token:?}");
let hash = generate_hash_sum(&token).await as i64;
let Ok(mut rows) = Sessions::find()
.filter(entities::sessions::Column::HashSum.eq(hash))
.all(&db_client)
.await
else {
return Err(ValidationError::FetchSessions);
};
let Some(found_idx) = rows.iter().position(|row| token == *row).clone() else {
return Err(ValidationError::UnknownToken);
};
let found = rows.remove(found_idx);
if found.expiration_time < Utc::now().naive_utc() {
return Err(ValidationError::Expired);
}
match Accounts::find()
.filter(database::accounts::Column::Id.eq(found.subject))
.one(&db_client)
.await
{
Err(e) => {
error!("Failed to load account for {found:?}: {e}");
Err(ValidationError::FetchAccounts)
}
Ok(None) => Err(ValidationError::NoAccount),
Ok(Some(account)) => Ok(account),
}
}
pub async fn create_jwt_string(
keys: JwtKeys,
claims: &Claims,
) -> Result<String, jsonwebtoken::errors::Error> {
jsonwebtoken::encode(
&jsonwebtoken::Header::new(Algorithm::EdDSA),
claims,
&keys.encode,
)
}
#[cfg(test)]
mod tests {
use database::chrono::Utc;
use super::*;
use std::path::Path;
#[tokio::test]
async fn create_string() {
let keys = JwtKeys::load(Path::new("./config").to_owned()).unwrap();
let claims = Claims::new(
234,
"jaosidf".into(),
"User".into(),
None,
Utc::now().naive_utc(),
Utc::now().naive_utc(),
None,
)
.await
.unwrap();
let _text = create_jwt_string(keys, &claims).await.unwrap();
}
}

16
crates/agent/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "agent"
version = "0.1.0"
edition = "2021"
[features]
with-web = ["web"]
with-database = ["database"]
with-events = ["events"]
[dependencies]
futures = { version = "0.3.30", default-features = false, features = ["async-await", "futures-executor", "executor", "compat", "std", "thread-pool"] }
tokio = { version = "1.35.1", features = ["full", "tracing"] }
web = { workspace = true, optional = true }
events = { workspace = true, optional = true }
database = { workspace = true, optional = true }

4
crates/agent/src/lib.rs Normal file
View File

@ -0,0 +1,4 @@
pub struct SharedResources {
#[cfg(feature = "web")]
web: web::Web,
}

12
crates/web/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "web"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix = "0.13.1"
actix-web = "4.4.1"
futures = { version = "0.3.30", default-features = false, features = ["async-await", "futures-executor", "executor", "compat", "std", "thread-pool"] }
tokio = { version = "1.35.1", features = ["full", "tracing"] }

4
crates/web/src/lib.rs Normal file
View File

@ -0,0 +1,4 @@
pub struct Web {}
impl Web {
}