use chrono::{DateTime, Utc};
use mas_iana::jose::JsonWebSignatureAlg;
use oauth2_types::scope::Scope;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use ulid::Ulid;
use url::Url;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum DiscoveryMode {
#[default]
Oidc,
Insecure,
Disabled,
}
impl DiscoveryMode {
#[must_use]
pub fn is_disabled(&self) -> bool {
matches!(self, DiscoveryMode::Disabled)
}
}
#[derive(Debug, Clone, Error)]
#[error("Invalid discovery mode {0:?}")]
pub struct InvalidDiscoveryModeError(String);
impl std::str::FromStr for DiscoveryMode {
type Err = InvalidDiscoveryModeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"oidc" => Ok(Self::Oidc),
"insecure" => Ok(Self::Insecure),
"disabled" => Ok(Self::Disabled),
s => Err(InvalidDiscoveryModeError(s.to_owned())),
}
}
}
impl DiscoveryMode {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Oidc => "oidc",
Self::Insecure => "insecure",
Self::Disabled => "disabled",
}
}
}
impl std::fmt::Display for DiscoveryMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum PkceMode {
#[default]
Auto,
S256,
Disabled,
}
#[derive(Debug, Clone, Error)]
#[error("Invalid PKCE mode {0:?}")]
pub struct InvalidPkceModeError(String);
impl std::str::FromStr for PkceMode {
type Err = InvalidPkceModeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"auto" => Ok(Self::Auto),
"s256" => Ok(Self::S256),
"disabled" => Ok(Self::Disabled),
s => Err(InvalidPkceModeError(s.to_owned())),
}
}
}
impl PkceMode {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::S256 => "s256",
Self::Disabled => "disabled",
}
}
}
impl std::fmt::Display for PkceMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Error)]
#[error("Invalid response mode {0:?}")]
pub struct InvalidResponseModeError(String);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ResponseMode {
#[default]
Query,
FormPost,
}
impl From<ResponseMode> for oauth2_types::requests::ResponseMode {
fn from(value: ResponseMode) -> Self {
match value {
ResponseMode::Query => oauth2_types::requests::ResponseMode::Query,
ResponseMode::FormPost => oauth2_types::requests::ResponseMode::FormPost,
}
}
}
impl ResponseMode {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Query => "query",
Self::FormPost => "form_post",
}
}
}
impl std::fmt::Display for ResponseMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for ResponseMode {
type Err = InvalidResponseModeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"query" => Ok(ResponseMode::Query),
"form_post" => Ok(ResponseMode::FormPost),
s => Err(InvalidResponseModeError(s.to_owned())),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TokenAuthMethod {
None,
ClientSecretBasic,
ClientSecretPost,
ClientSecretJwt,
PrivateKeyJwt,
SignInWithApple,
}
impl TokenAuthMethod {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::ClientSecretBasic => "client_secret_basic",
Self::ClientSecretPost => "client_secret_post",
Self::ClientSecretJwt => "client_secret_jwt",
Self::PrivateKeyJwt => "private_key_jwt",
Self::SignInWithApple => "sign_in_with_apple",
}
}
}
impl std::fmt::Display for TokenAuthMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for TokenAuthMethod {
type Err = InvalidUpstreamOAuth2TokenAuthMethod;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"none" => Ok(Self::None),
"client_secret_post" => Ok(Self::ClientSecretPost),
"client_secret_basic" => Ok(Self::ClientSecretBasic),
"client_secret_jwt" => Ok(Self::ClientSecretJwt),
"private_key_jwt" => Ok(Self::PrivateKeyJwt),
"sign_in_with_apple" => Ok(Self::SignInWithApple),
s => Err(InvalidUpstreamOAuth2TokenAuthMethod(s.to_owned())),
}
}
}
#[derive(Debug, Clone, Error)]
#[error("Invalid upstream OAuth 2.0 token auth method: {0}")]
pub struct InvalidUpstreamOAuth2TokenAuthMethod(String);
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct UpstreamOAuthProvider {
pub id: Ulid,
pub issuer: String,
pub human_name: Option<String>,
pub brand_name: Option<String>,
pub discovery_mode: DiscoveryMode,
pub pkce_mode: PkceMode,
pub jwks_uri_override: Option<Url>,
pub authorization_endpoint_override: Option<Url>,
pub scope: Scope,
pub token_endpoint_override: Option<Url>,
pub client_id: String,
pub encrypted_client_secret: Option<String>,
pub token_endpoint_signing_alg: Option<JsonWebSignatureAlg>,
pub token_endpoint_auth_method: TokenAuthMethod,
pub response_mode: ResponseMode,
pub created_at: DateTime<Utc>,
pub disabled_at: Option<DateTime<Utc>>,
pub claims_imports: ClaimsImports,
pub additional_authorization_parameters: Vec<(String, String)>,
}
impl PartialOrd for UpstreamOAuthProvider {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.id.cmp(&other.id))
}
}
impl Ord for UpstreamOAuthProvider {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.id.cmp(&other.id)
}
}
impl UpstreamOAuthProvider {
#[must_use]
pub const fn enabled(&self) -> bool {
self.disabled_at.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum SetEmailVerification {
Always,
Never,
#[default]
Import,
}
impl SetEmailVerification {
#[must_use]
pub fn should_mark_as_verified(&self, upstream_verified: bool) -> bool {
match self {
Self::Always => true,
Self::Never => false,
Self::Import => upstream_verified,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ClaimsImports {
#[serde(default)]
pub subject: SubjectPreference,
#[serde(default)]
pub localpart: ImportPreference,
#[serde(default)]
pub displayname: ImportPreference,
#[serde(default)]
pub email: ImportPreference,
#[serde(default)]
pub verify_email: SetEmailVerification,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SubjectPreference {
#[serde(default)]
pub template: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ImportPreference {
#[serde(default)]
pub action: ImportAction,
#[serde(default)]
pub template: Option<String>,
}
impl std::ops::Deref for ImportPreference {
type Target = ImportAction;
fn deref(&self) -> &Self::Target {
&self.action
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ImportAction {
#[default]
Ignore,
Suggest,
Force,
Require,
}
impl ImportAction {
#[must_use]
pub fn is_forced(&self) -> bool {
matches!(self, Self::Force | Self::Require)
}
#[must_use]
pub fn ignore(&self) -> bool {
matches!(self, Self::Ignore)
}
#[must_use]
pub fn is_required(&self) -> bool {
matches!(self, Self::Require)
}
#[must_use]
pub fn should_import(&self, user_preference: bool) -> bool {
match self {
Self::Ignore => false,
Self::Suggest => user_preference,
Self::Force | Self::Require => true,
}
}
}