Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework Id type #90

Merged
merged 6 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 1 addition & 18 deletions Cargo.lock

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

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
2 changes: 1 addition & 1 deletion iam-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
186 changes: 125 additions & 61 deletions iam-common/src/id.rs
Original file line number Diff line number Diff line change
@@ -1,52 +1,56 @@
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,
User,
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<Self, Self::Err> {
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<Context> = 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,
}
}
Expand All @@ -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<Self, Self::Err> {
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 })
}
}

Expand Down Expand Up @@ -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);
}
}
1 change: 0 additions & 1 deletion iam/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
35 changes: 12 additions & 23 deletions libiam/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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::<Claims>(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::<Id>(
format!(
// HACK: impl FromStr
"\"{}\"",
jsonwebtoken::decode::<Claims>(
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,
}),
})
Expand Down
Loading