diff --git a/crates/core/database/src/models/bots/model.rs b/crates/core/database/src/models/bots/model.rs index d145163c..2a2ad0b5 100644 --- a/crates/core/database/src/models/bots/model.rs +++ b/crates/core/database/src/models/bots/model.rs @@ -1,5 +1,6 @@ use revolt_result::Result; use ulid::Ulid; +use std::collections::HashMap; use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User}; @@ -46,6 +47,19 @@ auto_derived_partial!( "PartialBot" ); +auto_derived!( + #[derive(Copy, Hash)] + pub enum OAuth2Scope { + Identify, + Full, + } + + pub struct OAuth2ScopeReasoning { + pub allow: String, + pub deny: String + } +); + auto_derived_partial!( pub struct BotOauth2 { /// Whether the oauth2 client is public and should not receive a secret key @@ -56,7 +70,10 @@ auto_derived_partial!( pub secret: Option, /// Allowed redirects for the authorisation #[serde(default)] - pub redirects: Vec + pub redirects: Vec, + /// Mapping of allowed scopes and the reasonings + #[serde(default)] + pub allowed_scopes: HashMap, }, "PartialBotOauth2" ); @@ -96,7 +113,8 @@ impl Default for BotOauth2 { Self { public: false, secret: Some(nanoid::nanoid!(64)), - redirects: Default::default() + redirects: Vec::new(), + allowed_scopes: HashMap::new(), } } } diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index f7b62e99..0613cb99 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -67,6 +67,10 @@ impl From for crate::BotOauth2 { public: value.public, secret: value.secret, redirects: value.redirects, + allowed_scopes: value.allowed_scopes + .into_iter() + .map(|(scope, value)| (scope.into(), value.into())) + .collect(), } } } @@ -77,6 +81,46 @@ impl From for BotOauth2 { public: value.public, secret: value.secret, redirects: value.redirects, + allowed_scopes: value.allowed_scopes + .into_iter() + .map(|(scope, value)| (scope.into(), value.into())) + .collect(), + } + } +} + +impl From for OAuth2Scope { + fn from(value: crate::OAuth2Scope) -> Self { + match value { + crate::OAuth2Scope::Identify => OAuth2Scope::Identify, + crate::OAuth2Scope::Full => OAuth2Scope::Full, + } + } +} + +impl From for crate::OAuth2Scope { + fn from(value: OAuth2Scope) -> Self { + match value { + OAuth2Scope::Identify => crate::OAuth2Scope::Identify, + OAuth2Scope::Full => crate::OAuth2Scope::Full, + } + } +} + +impl From for OAuth2ScopeReasoning { + fn from(value: crate::OAuth2ScopeReasoning) -> Self { + OAuth2ScopeReasoning { + allow: value.allow, + deny: value.deny, + } + } +} + +impl From for crate::OAuth2ScopeReasoning { + fn from(value: OAuth2ScopeReasoning) -> Self { + crate::OAuth2ScopeReasoning { + allow: value.allow, + deny: value.deny, } } } diff --git a/crates/core/database/src/util/oauth2.rs b/crates/core/database/src/util/oauth2.rs index 6cc136e0..ef09be01 100644 --- a/crates/core/database/src/util/oauth2.rs +++ b/crates/core/database/src/util/oauth2.rs @@ -2,49 +2,12 @@ use chrono::{TimeDelta, Utc}; use jsonwebtoken::{decode, encode, errors::Error, DecodingKey, EncodingKey, Header, Validation}; use serde::{Serialize, Deserialize}; use revolt_models::v0; +use redis_kiss::AsyncCommands; +use revolt_result::Result; #[cfg(feature = "rocket")] use rocket::{http::Method, Request}; -// use redis_kiss::{get_connection, redis::Pipeline, AsyncCommands}; -// use revolt_result::Result; - -// pub async fn add_access_token(code: &str, redirect_uri: &str, client_id: &str) -> Result<()> { -// let conn = get_connection() -// .await -// .map_err(|_| create_error!(InternalError))?; - -// // 10 mins -// Pipeline::new() -// .pset_ex(format!("oauth:{code}:client_id"), client_id, 600000) -// .pset_ex(format!("oauth:{code}:redirect_uri"), redirect_uri, 600000) -// .query_async::<_, ()>(&mut conn.into_inner()) -// .await -// .map_err(|_| create_error!(InternalError))?; - -// Ok(()) -// } - -// pub struct CodeInfo { -// pub redirect_uri: String, -// pub client_id: String -// } - -// pub async fn get_access_token_info(code: &str) -> Result { -// let conn = get_connection() -// .await -// .map_err(|_| create_error!(InternalError))?; - -// let (redirect_uri, client_id) = Pipeline::new() -// .get(format!("oauth:{code}:client_id")) -// .get(format!("oauth:{code}:redirect_uri")) -// .query_async::<_, (String, String)>(&mut conn.into_inner()) -// .await -// .map_err(|_| create_error!(InternalError))?; - -// Ok(CodeInfo { redirect_uri, client_id }) -// } - #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum TokenType { Auth, @@ -144,4 +107,20 @@ pub fn scopes_can_access_route(scopes: &str, request: &Request<'_>) -> bool { } false +} + +pub async fn add_code_challange(token: &str, code_challenge: &str) -> Result<()> { + let mut conn = redis_kiss::get_connection() + .await + .map_err(|_| create_error!(InternalError))?; + + conn.pset_ex::<_, _, ()>( + format!("oauth2:{token}:code_challenge"), + code_challenge, + TokenType::Access.lifetime().num_milliseconds() as usize + ) + .await + .map_err(|_| create_error!(InternalError))?; + + Ok(()) } \ No newline at end of file diff --git a/crates/core/models/src/v0/bots.rs b/crates/core/models/src/v0/bots.rs index 9c38eb23..4b8ddc07 100644 --- a/crates/core/models/src/v0/bots.rs +++ b/crates/core/models/src/v0/bots.rs @@ -1,4 +1,5 @@ -use super::User; +use super::{User, OAuth2Scope, OAuth2ScopeReasoning}; +use std::collections::HashMap; #[cfg(feature = "validator")] use validator::Validate; @@ -155,10 +156,10 @@ auto_derived!( #[derive(Default)] #[cfg_attr(feature = "validator", derive(Validate))] pub struct DataEditBotOauth2 { - #[cfg_attr(feature = "serde", serde(default))] pub public: Option, #[cfg_attr(feature = "validator", validate(length(min = 1, max = 10)))] pub redirects: Option>, + pub allowed_scopes: Option> } /// Where we are inviting a bot to @@ -207,7 +208,10 @@ auto_derived_partial!( pub secret: Option, /// Allowed redirects for the authorisation #[serde(default)] - pub redirects: Vec + pub redirects: Vec, + /// Mapping of allowed scopes and the reasonings + #[serde(default)] + pub allowed_scopes: HashMap, }, "PartialBotOauth2" ); \ No newline at end of file diff --git a/crates/core/models/src/v0/oauth2.rs b/crates/core/models/src/v0/oauth2.rs index 91df6554..c82715d4 100644 --- a/crates/core/models/src/v0/oauth2.rs +++ b/crates/core/models/src/v0/oauth2.rs @@ -1,4 +1,5 @@ use crate::v0::{PublicBot, User}; +use std::collections::HashMap; auto_derived!( pub struct OAuth2AuthorizeAuthResponse { @@ -6,9 +7,15 @@ auto_derived!( pub redirect_uri: String, } + pub struct OAuth2ScopeReasoning { + pub allow: String, + pub deny: String + } + pub struct OAuth2AuthorizeInfoResponse { pub bot: PublicBot, pub user: User, + pub allowed_scopes: HashMap } #[derive(Copy)] @@ -69,6 +76,7 @@ auto_derived!( pub scope: String, } + #[derive(Copy, Hash)] pub enum OAuth2Scope { Identify, Full, diff --git a/crates/delta/src/routes/bots/edit.rs b/crates/delta/src/routes/bots/edit.rs index 6338dd59..845c7819 100644 --- a/crates/delta/src/routes/bots/edit.rs +++ b/crates/delta/src/routes/bots/edit.rs @@ -77,6 +77,13 @@ pub async fn edit_bot( } oauth2.redirects = edit_oauth2.redirects.unwrap_or(oauth2.redirects); + oauth2.allowed_scopes = edit_oauth2.allowed_scopes + .map(|scopes|scopes + .into_iter() + .map(|(scope, value)| (scope.into(), value.into())) + .collect() + ) + .unwrap_or(oauth2.allowed_scopes); partial.oauth2 = Some(oauth2) } diff --git a/crates/delta/src/routes/oauth2/authorize_auth.rs b/crates/delta/src/routes/oauth2/authorize_auth.rs index 2f149a83..682d6e35 100644 --- a/crates/delta/src/routes/oauth2/authorize_auth.rs +++ b/crates/delta/src/routes/oauth2/authorize_auth.rs @@ -1,7 +1,6 @@ use revolt_config::config; use revolt_database::{ - util::{oauth2, reference::Reference}, - Database, User, + util::{oauth2, reference::Reference}, Database, User }; use revolt_models::v0; use revolt_result::{create_error, Result}; @@ -26,13 +25,20 @@ pub async fn auth( return Err(create_error!(InvalidOperation)); }; - if !oauth2.redirects.contains(&info.redirect_uri) || v0::OAuth2Scope::scopes_from_str(&info.scope).is_none() { + let Some(scopes) = v0::OAuth2Scope::scopes_from_str(&info.scope) else { + return Err(create_error!(InvalidOperation)); + }; + + if scopes.into_iter().any(|scope| !oauth2.allowed_scopes.contains_key(&scope.into())) + || !oauth2.redirects.contains(&info.redirect_uri) + || v0::OAuth2Scope::scopes_from_str(&info.scope).is_none() + { return Err(create_error!(InvalidOperation)); }; let config = config().await; - let code = match info.response_type { + let token = match info.response_type { // implicit v0::OAuth2ResponseType::Code => { if info.state.is_some() || info.code_challenge.is_some() || info.code_challenge_method.is_some() { @@ -66,7 +72,7 @@ pub async fn auth( return Err(create_error!(InvalidOperation)) }; - let code = oauth2::encode_token( + let token = oauth2::encode_token( &config.api.security.token_secret, oauth2::TokenType::Access, user.id.clone(), @@ -78,25 +84,15 @@ pub async fn auth( .map_err(|_| create_error!(InternalError))?; if let Some(code_challenge) = info.code_challenge.as_ref() { - let mut conn = redis_kiss::get_connection() - .await - .map_err(|_| create_error!(InternalError))?; - - conn.pset_ex::<_, _, ()>( - format!("oauth2:{code}:code_challenge"), - code_challenge, - oauth2::TokenType::Access.lifetime().num_milliseconds() as usize - ) - .await - .map_err(|_| create_error!(InternalError))?; + oauth2::add_code_challange(&token, code_challenge).await?; }; - code + token }, }; - let redirect_uri = format!("{}/?code={code}", &info.redirect_uri); + let redirect_uri = format!("{}/?code={token}", &info.redirect_uri); Ok(Json(v0::OAuth2AuthorizeAuthResponse { redirect_uri })) } diff --git a/crates/delta/src/routes/oauth2/authorize_info.rs b/crates/delta/src/routes/oauth2/authorize_info.rs index 2c4a9459..f1d7a91b 100644 --- a/crates/delta/src/routes/oauth2/authorize_info.rs +++ b/crates/delta/src/routes/oauth2/authorize_info.rs @@ -27,6 +27,10 @@ pub async fn info( Ok(Json(v0::OAuth2AuthorizeInfoResponse { bot: public_bot, - user: user.into(db, None).await + user: user.into(db, None).await, + allowed_scopes: oauth2.allowed_scopes.clone() + .into_iter() + .map(|(scope, value)| (scope.into(), value.into())) + .collect() })) } \ No newline at end of file