From e048d7d050f87e9e5bf06f01e39fd417d6868c7e Mon Sep 17 00:00:00 2001 From: mrw1593 Date: Mon, 29 May 2023 10:51:10 -0400 Subject: Create a Client struct --- src/api/users.rs | 2 +- src/models/client.rs | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/models/mod.rs | 5 +-- src/services/db.rs | 2 +- 4 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 src/models/client.rs (limited to 'src') diff --git a/src/api/users.rs b/src/api/users.rs index 353f8ff..2b67663 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -6,7 +6,7 @@ use sqlx::MySqlPool; use thiserror::Error; use uuid::Uuid; -use crate::models::User; +use crate::models::user::User; use crate::services::crypto::PasswordHash; use crate::services::{db, id}; diff --git a/src/models/client.rs b/src/models/client.rs new file mode 100644 index 0000000..a7df936 --- /dev/null +++ b/src/models/client.rs @@ -0,0 +1,111 @@ +use std::{hash::Hash, marker::PhantomData}; + +use exun::{Expect, RawUnexpected}; +use raise::yeet; +use thiserror::Error; +use url::Url; +use uuid::Uuid; + +use crate::services::crypto::PasswordHash; + +/// There are two types of clients, based on their ability to maintain the +/// security of their client credentials. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type)] +#[sqlx(rename_all = "lowercase")] +pub enum ClientType { + /// A client that is capable of maintaining the confidentiality of their + /// credentials, or capable of secure client authentication using other + /// means. An example would be a secure server with restricted access to + /// the client credentials. + Confidential, + /// A client that is incapable of maintaining the confidentiality of their + /// credentials and cannot authenticate securely by any other means, such + /// as an installed application, or a web-browser based application. + Public, +} + +#[derive(Debug, Clone)] +pub struct Client { + ty: ClientType, + id: Uuid, + secret: Option, + redirect_uris: Box<[Url]>, +} + +impl PartialEq for Client { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Client {} + +impl Hash for Client { + fn hash(&self, state: &mut H) { + state.write_u128(self.id.as_u128()) + } +} + +#[derive(Debug, Clone, Copy, Error)] +#[error("Confidential clients must have a secret, but it was not provided")] +pub struct NoSecretError { + _phantom: PhantomData<()>, +} + +impl NoSecretError { + fn new() -> Self { + Self { + _phantom: PhantomData, + } + } +} + +impl Client { + pub fn new_public( + id: Uuid, + ty: ClientType, + secret: Option<&str>, + redirect_uris: &[Url], + ) -> Result> { + let secret = if let Some(secret) = secret { + Some(PasswordHash::new(secret)?) + } else { + None + }; + + if ty == ClientType::Confidential && secret.is_none() { + yeet!(NoSecretError::new().into()); + } + + Ok(Self { + id, + ty: ClientType::Public, + secret, + redirect_uris: redirect_uris.into_iter().cloned().collect(), + }) + } + + pub fn id(&self) -> Uuid { + self.id + } + + pub fn client_type(&self) -> ClientType { + self.ty + } + + pub fn secret_hash(&self) -> Option<&[u8]> { + self.secret.as_ref().map(|s| s.hash()) + } + + pub fn secret_salt(&self) -> Option<&[u8]> { + self.secret.as_ref().map(|s| s.salt()) + } + + pub fn secret_version(&self) -> Option { + self.secret.as_ref().map(|s| s.version()) + } + + pub fn check_secret(&self, secret: &str) -> Option> { + self.secret.as_ref().map(|s| s.check_password(secret)) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 4a9be81..633f846 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,2 @@ -mod user; - -pub use user::User; +pub mod client; +pub mod user; diff --git a/src/services/db.rs b/src/services/db.rs index f4da004..79df260 100644 --- a/src/services/db.rs +++ b/src/services/db.rs @@ -2,7 +2,7 @@ use exun::*; use sqlx::{mysql::MySqlQueryResult, query, query_as, query_scalar, Executor, MySql, MySqlPool}; use uuid::Uuid; -use crate::models::User; +use crate::models::user::User; use super::crypto::PasswordHash; -- cgit v1.2.3