diff --git a/Cargo.lock b/Cargo.lock index c1d8dbb..633b4b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,15 +206,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atomic" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" -dependencies = [ - "bytemuck", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -412,12 +403,6 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" -[[package]] -name = "bytemuck" -version = "1.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" - [[package]] name = "byteorder" version = "1.5.0" @@ -1234,7 +1219,6 @@ dependencies = [ "iam-common", "iam-entity", "mime", - "once_cell", "pin-project-lite", "rand", "sea-orm", @@ -1272,6 +1256,7 @@ dependencies = [ name = "iam-common" version = "0.1.0" dependencies = [ + "anyhow", "axum", "base64 0.22.1", "bcrypt", @@ -1283,7 +1268,6 @@ dependencies = [ "jose-jwk", "jsonwebtoken", "mime", - "once_cell", "rand", "rust-argon2", "sea-orm", @@ -3471,7 +3455,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ - "atomic", "getrandom", ] diff --git a/Cargo.toml b/Cargo.toml index 9d7adbb..332a146 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,11 +34,10 @@ iam-entity = { path = "./iam-entity" } iam-macros = { path = "./iam-macros" } jsonwebtoken = "9.3.0" mime = "0.3.17" -once_cell = "1.19.0" rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } sea-orm = { version = "0.12.15", default-features = false, features = ["macros", "runtime-actix-rustls", "sqlx-mysql", "with-chrono"] } serde = { version = "1.0.204", features = ["derive"] } serde_json = "1.0.120" tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread", "signal"] } tracing = { version = "0.1.40", default-features = false } -uuid = { version = "1.10.0", features = ["v1", "v4", "rng"] } +uuid = { version = "1.10.0", default-features = false, features = ["v4"] } diff --git a/iam-common/Cargo.toml b/iam-common/Cargo.toml index a4b52c4..5df7923 100644 --- a/iam-common/Cargo.toml +++ b/iam-common/Cargo.toml @@ -13,7 +13,6 @@ rand.workspace = true rust-argon2 = { version = "2.1.0", default-features = false } bcrypt = "0.15.1" serde.workspace = true -once_cell.workspace = true jsonwebtoken.workspace = true axum.workspace = true iam-macros.workspace = true @@ -24,3 +23,4 @@ mime.workspace = true base64.workspace = true ed25519-dalek = { version = "2.1.1", features = ["pkcs8", "rand_core"] } jose-jwk = { version = "0.1.2", default-features = false } +anyhow.workspace = true diff --git a/iam-common/src/id.rs b/iam-common/src/id.rs index 8ee5245..8b526d8 100644 --- a/iam-common/src/id.rs +++ b/iam-common/src/id.rs @@ -1,13 +1,12 @@ -use once_cell::sync::Lazy; +use anyhow::bail; use serde::{Deserialize, Serialize}; -use std::fmt::{self, Display}; -use uuid::{ - v1::{Context, Timestamp}, - Uuid, +use std::{ + fmt::{self, Display}, + str::FromStr, }; +use uuid::Uuid; -#[derive(Debug, Clone, Copy)] -#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IdType { Action, Group, @@ -15,38 +14,43 @@ pub enum IdType { App, } +impl IdType { + const fn as_str(&self) -> &'static str { + match self { + IdType::Action => "ActionID", + IdType::Group => "GroupID", + IdType::User => "UserID", + IdType::App => "AppID", + } + } +} + +impl FromStr for IdType { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let ty = match s { + "ActionID" => IdType::Action, + "UserID" => IdType::User, + "AppID" => IdType::App, + "GroupID" => IdType::Group, + _ => bail!("invalid type"), + }; + + Ok(ty) + } +} + #[derive(Debug, Clone, Copy)] pub struct Id { uuid: Uuid, ty: IdType, } -static CONTEXT: Lazy = Lazy::new(Context::new_random); - impl Id { fn new(ty: IdType) -> Self { - let date = chrono::Utc::now(); - let timestamp = Timestamp::from_unix( - &*CONTEXT, - date.timestamp() as u64, - date.timestamp_subsec_nanos(), - ); - let hostname = std::env::var("HOSTNAME").unwrap_or_else(|_| "dev".to_string()); - - let mut buf = [0u8; 6]; - let mut buf_iter = buf.iter_mut(); - let mut iter = hostname.as_bytes().iter(); - - while let Some(x) = iter.next_back() { - if let Some(y) = buf_iter.next() { - *y = *x; - } else { - break; - } - } - Self { - uuid: Uuid::new_v1(timestamp, &buf), + uuid: Uuid::new_v4(), ty, } } @@ -72,26 +76,37 @@ impl Id { } #[inline] - const fn get_prefix(&self) -> &'static str { - match self.ty { - IdType::Action => "ActionID", - IdType::Group => "GroupID", - IdType::User => "UserID", - IdType::App => "AppID", - } + pub const fn get_type(&self) -> IdType { + self.ty } } impl Display for Id { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}-{}", - self.get_prefix(), - self.uuid - .as_hyphenated() - .encode_lower(&mut Uuid::encode_buffer()) - ) + let ty = self.get_type().as_str(); + + let mut buf = Uuid::encode_buffer(); + let uuid = self.uuid.as_hyphenated().encode_lower(&mut buf); + + write!(f, "{}-{}", ty, uuid) + } +} + +impl FromStr for Id { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let Some((ty, id_str)) = s.split_once('-') else { + bail!("invalid format"); + }; + + let ty = IdType::from_str(ty)?; + + let Ok(uuid) = Uuid::parse_str(id_str) else { + bail!("invalid uuid"); + }; + + Ok(Self { uuid, ty }) } } @@ -125,25 +140,74 @@ impl<'de> Deserialize<'de> for Id { where E: de::Error, { - let (ty, id_str) = if let Some(i) = v.strip_prefix("ActionID-") { - (IdType::Action, i) - } else if let Some(i) = v.strip_prefix("GroupID-") { - (IdType::Group, i) - } else if let Some(i) = v.strip_prefix("UserID-") { - (IdType::User, i) - } else if let Some(i) = v.strip_prefix("AppID-") { - (IdType::App, i) - } else { - return Err(de::Error::invalid_value(de::Unexpected::Str(v), &self)); - }; - - let uuid = Uuid::parse_str(id_str) - .map_err(|_| de::Error::invalid_value(de::Unexpected::Str(id_str), &self))?; - - Ok(Id { ty, uuid }) + match Id::from_str(v) { + Ok(id) => Ok(id), + Err(err) => Err(de::Error::custom(err)), + } } } deserializer.deserialize_str(Visitor) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_str() { + let id = Id::from_str("UserID-00000000-0000-0000-0000-000000000000").unwrap(); + assert_eq!(id.get_type(), IdType::User); + + let id = Id::from_str("ActionID-00000000-0000-0000-0000-000000000000").unwrap(); + assert_eq!(id.get_type(), IdType::Action); + + let id = Id::from_str("UserID-00000000-0000-0000-0000-000000000000").unwrap(); + assert_eq!(id.get_type(), IdType::User); + + let id = Id::from_str("GroupID-00000000-0000-0000-0000-000000000000").unwrap(); + assert_eq!(id.get_type(), IdType::Group); + } + + #[test] + fn from_str_invalid_type() { + let id = Id::from_str("Invalid-00000000-0000-0000-0000-000000000000"); + assert!(id.is_err()); + } + + #[test] + fn from_str_invalid_uuid() { + let id = Id::from_str("UserID-invalid"); + assert!(id.is_err()); + } + + #[test] + fn from_str_invalid_format() { + let id = Id::from_str("invalid"); + assert!(id.is_err()); + } + + #[test] + fn to_string() { + let str_id = "UserID-00000000-0000-0000-0000-000000000000"; + let id = Id::from_str(str_id).unwrap(); + + assert_eq!(id.to_string(), str_id); + + let str_id = "ActionID-00000000-0000-0000-0000-000000000000"; + let id = Id::from_str(str_id).unwrap(); + + assert_eq!(id.to_string(), str_id); + + let str_id = "AppID-00000000-0000-0000-0000-000000000000"; + let id = Id::from_str(str_id).unwrap(); + + assert_eq!(id.to_string(), str_id); + + let str_id = "GroupID-00000000-0000-0000-0000-000000000000"; + let id = Id::from_str(str_id).unwrap(); + + assert_eq!(id.to_string(), str_id); + } +} diff --git a/iam/Cargo.toml b/iam/Cargo.toml index 8c9f49d..855c056 100644 --- a/iam/Cargo.toml +++ b/iam/Cargo.toml @@ -19,7 +19,6 @@ chrono.workspace = true validator = { version = "0.18.1", features = ["derive"] } async-trait.workspace = true futures-util = "0.3.30" -once_cell.workspace = true tokio.workspace = true tower = { version = "0.4.13", features = ["timeout"] } tower-http = { version = "0.5.2", features = ["add-extension", "auth", "compression-full", "cors", "decompression-full", "request-id", "sensitive-headers", "trace", "util"] } diff --git a/libiam/src/user.rs b/libiam/src/user.rs index 545e9fb..fbf6797 100644 --- a/libiam/src/user.rs +++ b/libiam/src/user.rs @@ -4,7 +4,7 @@ use crate::{ }; use iam_common::{keys::jwt::Claims, Id}; use jsonwebtoken::{Algorithm, DecodingKey, Validation}; -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; #[derive(Debug)] pub struct UserInner { @@ -48,31 +48,20 @@ impl User { let api = iam.inner.api.with_token(token.clone()); + // TODO: this should be done with `Jwt::get_claims()` + let claims = + jsonwebtoken::decode::(token.as_str(), &DecodingKey::from_secret(&[]), &{ + let mut v = Validation::new(Algorithm::EdDSA); + v.insecure_disable_signature_validation(); + v.set_audience(&["https://verseghy-gimnazium.net"]); + v + })? + .claims; + Ok(Self { inner: Arc::new(UserInner { token: token.clone(), - id: serde_json::from_str::( - format!( - // HACK: impl FromStr - "\"{}\"", - jsonwebtoken::decode::( - token.as_str(), - &DecodingKey::from_secret(&[]), - &{ - let mut v = Validation::new(Algorithm::RS256); - v.insecure_disable_signature_validation(); - v.set_audience(&["https://verseghy-gimnazium.net"]); - v - }, - ) - .unwrap() - .claims - .sub - .as_str() - ) - .as_str(), - ) - .unwrap(), + id: Id::from_str(&claims.sub)?, _api: api, }), })