Add agents
This commit is contained in:
parent
765c8753dd
commit
e054860091
10
bins/identity-agent/src/migration/mod.rs
Normal file
10
bins/identity-agent/src/migration/mod.rs
Normal 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![]
|
||||
}
|
||||
}
|
12
bins/sessions-agent/Cargo.toml
Normal file
12
bins/sessions-agent/Cargo.toml
Normal 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"
|
3
bins/sessions-agent/src/main.rs
Normal file
3
bins/sessions-agent/src/main.rs
Normal file
@ -0,0 +1,3 @@
|
||||
#[actix::main]
|
||||
async fn main() {
|
||||
}
|
10
bins/sessions-agent/src/migration/mod.rs
Normal file
10
bins/sessions-agent/src/migration/mod.rs
Normal 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![]
|
||||
}
|
||||
}
|
294
bins/sessions-agent/src/utils.rs
Normal file
294
bins/sessions-agent/src/utils.rs
Normal 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
16
crates/agent/Cargo.toml
Normal 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
4
crates/agent/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub struct SharedResources {
|
||||
#[cfg(feature = "web")]
|
||||
web: web::Web,
|
||||
}
|
12
crates/web/Cargo.toml
Normal file
12
crates/web/Cargo.toml
Normal 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
4
crates/web/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub struct Web {}
|
||||
|
||||
impl Web {
|
||||
}
|
Loading…
Reference in New Issue
Block a user