feat: allowed scopes
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
|
use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
|
||||||
|
|
||||||
@@ -46,6 +47,19 @@ auto_derived_partial!(
|
|||||||
"PartialBot"
|
"PartialBot"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
auto_derived!(
|
||||||
|
#[derive(Copy, Hash)]
|
||||||
|
pub enum OAuth2Scope {
|
||||||
|
Identify,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OAuth2ScopeReasoning {
|
||||||
|
pub allow: String,
|
||||||
|
pub deny: String
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
auto_derived_partial!(
|
auto_derived_partial!(
|
||||||
pub struct BotOauth2 {
|
pub struct BotOauth2 {
|
||||||
/// Whether the oauth2 client is public and should not receive a secret key
|
/// Whether the oauth2 client is public and should not receive a secret key
|
||||||
@@ -56,7 +70,10 @@ auto_derived_partial!(
|
|||||||
pub secret: Option<String>,
|
pub secret: Option<String>,
|
||||||
/// Allowed redirects for the authorisation
|
/// Allowed redirects for the authorisation
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub redirects: Vec<String>
|
pub redirects: Vec<String>,
|
||||||
|
/// Mapping of allowed scopes and the reasonings
|
||||||
|
#[serde(default)]
|
||||||
|
pub allowed_scopes: HashMap<OAuth2Scope, OAuth2ScopeReasoning>,
|
||||||
},
|
},
|
||||||
"PartialBotOauth2"
|
"PartialBotOauth2"
|
||||||
);
|
);
|
||||||
@@ -96,7 +113,8 @@ impl Default for BotOauth2 {
|
|||||||
Self {
|
Self {
|
||||||
public: false,
|
public: false,
|
||||||
secret: Some(nanoid::nanoid!(64)),
|
secret: Some(nanoid::nanoid!(64)),
|
||||||
redirects: Default::default()
|
redirects: Vec::new(),
|
||||||
|
allowed_scopes: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ impl From<BotOauth2> for crate::BotOauth2 {
|
|||||||
public: value.public,
|
public: value.public,
|
||||||
secret: value.secret,
|
secret: value.secret,
|
||||||
redirects: value.redirects,
|
redirects: value.redirects,
|
||||||
|
allowed_scopes: value.allowed_scopes
|
||||||
|
.into_iter()
|
||||||
|
.map(|(scope, value)| (scope.into(), value.into()))
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +81,46 @@ impl From<crate::BotOauth2> for BotOauth2 {
|
|||||||
public: value.public,
|
public: value.public,
|
||||||
secret: value.secret,
|
secret: value.secret,
|
||||||
redirects: value.redirects,
|
redirects: value.redirects,
|
||||||
|
allowed_scopes: value.allowed_scopes
|
||||||
|
.into_iter()
|
||||||
|
.map(|(scope, value)| (scope.into(), value.into()))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::OAuth2Scope> for OAuth2Scope {
|
||||||
|
fn from(value: crate::OAuth2Scope) -> Self {
|
||||||
|
match value {
|
||||||
|
crate::OAuth2Scope::Identify => OAuth2Scope::Identify,
|
||||||
|
crate::OAuth2Scope::Full => OAuth2Scope::Full,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OAuth2Scope> for crate::OAuth2Scope {
|
||||||
|
fn from(value: OAuth2Scope) -> Self {
|
||||||
|
match value {
|
||||||
|
OAuth2Scope::Identify => crate::OAuth2Scope::Identify,
|
||||||
|
OAuth2Scope::Full => crate::OAuth2Scope::Full,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::OAuth2ScopeReasoning> for OAuth2ScopeReasoning {
|
||||||
|
fn from(value: crate::OAuth2ScopeReasoning) -> Self {
|
||||||
|
OAuth2ScopeReasoning {
|
||||||
|
allow: value.allow,
|
||||||
|
deny: value.deny,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OAuth2ScopeReasoning> for crate::OAuth2ScopeReasoning {
|
||||||
|
fn from(value: OAuth2ScopeReasoning) -> Self {
|
||||||
|
crate::OAuth2ScopeReasoning {
|
||||||
|
allow: value.allow,
|
||||||
|
deny: value.deny,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,49 +2,12 @@ use chrono::{TimeDelta, Utc};
|
|||||||
use jsonwebtoken::{decode, encode, errors::Error, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{decode, encode, errors::Error, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use revolt_models::v0;
|
use revolt_models::v0;
|
||||||
|
use redis_kiss::AsyncCommands;
|
||||||
|
use revolt_result::Result;
|
||||||
|
|
||||||
#[cfg(feature = "rocket")]
|
#[cfg(feature = "rocket")]
|
||||||
use rocket::{http::Method, Request};
|
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<CodeInfo> {
|
|
||||||
// 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)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum TokenType {
|
pub enum TokenType {
|
||||||
Auth,
|
Auth,
|
||||||
@@ -144,4 +107,20 @@ pub fn scopes_can_access_route(scopes: &str, request: &Request<'_>) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
false
|
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(())
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::User;
|
use super::{User, OAuth2Scope, OAuth2ScopeReasoning};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[cfg(feature = "validator")]
|
#[cfg(feature = "validator")]
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
@@ -155,10 +156,10 @@ auto_derived!(
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||||
pub struct DataEditBotOauth2 {
|
pub struct DataEditBotOauth2 {
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
|
||||||
pub public: Option<bool>,
|
pub public: Option<bool>,
|
||||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 10)))]
|
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 10)))]
|
||||||
pub redirects: Option<Vec<String>>,
|
pub redirects: Option<Vec<String>>,
|
||||||
|
pub allowed_scopes: Option<HashMap<OAuth2Scope, OAuth2ScopeReasoning>>
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where we are inviting a bot to
|
/// Where we are inviting a bot to
|
||||||
@@ -207,7 +208,10 @@ auto_derived_partial!(
|
|||||||
pub secret: Option<String>,
|
pub secret: Option<String>,
|
||||||
/// Allowed redirects for the authorisation
|
/// Allowed redirects for the authorisation
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub redirects: Vec<String>
|
pub redirects: Vec<String>,
|
||||||
|
/// Mapping of allowed scopes and the reasonings
|
||||||
|
#[serde(default)]
|
||||||
|
pub allowed_scopes: HashMap<OAuth2Scope, OAuth2ScopeReasoning>,
|
||||||
},
|
},
|
||||||
"PartialBotOauth2"
|
"PartialBotOauth2"
|
||||||
);
|
);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::v0::{PublicBot, User};
|
use crate::v0::{PublicBot, User};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
auto_derived!(
|
auto_derived!(
|
||||||
pub struct OAuth2AuthorizeAuthResponse {
|
pub struct OAuth2AuthorizeAuthResponse {
|
||||||
@@ -6,9 +7,15 @@ auto_derived!(
|
|||||||
pub redirect_uri: String,
|
pub redirect_uri: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct OAuth2ScopeReasoning {
|
||||||
|
pub allow: String,
|
||||||
|
pub deny: String
|
||||||
|
}
|
||||||
|
|
||||||
pub struct OAuth2AuthorizeInfoResponse {
|
pub struct OAuth2AuthorizeInfoResponse {
|
||||||
pub bot: PublicBot,
|
pub bot: PublicBot,
|
||||||
pub user: User,
|
pub user: User,
|
||||||
|
pub allowed_scopes: HashMap<OAuth2Scope, OAuth2ScopeReasoning>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy)]
|
#[derive(Copy)]
|
||||||
@@ -69,6 +76,7 @@ auto_derived!(
|
|||||||
pub scope: String,
|
pub scope: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Hash)]
|
||||||
pub enum OAuth2Scope {
|
pub enum OAuth2Scope {
|
||||||
Identify,
|
Identify,
|
||||||
Full,
|
Full,
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ pub async fn edit_bot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
oauth2.redirects = edit_oauth2.redirects.unwrap_or(oauth2.redirects);
|
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)
|
partial.oauth2 = Some(oauth2)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use revolt_config::config;
|
use revolt_config::config;
|
||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
util::{oauth2, reference::Reference},
|
util::{oauth2, reference::Reference}, Database, User
|
||||||
Database, User,
|
|
||||||
};
|
};
|
||||||
use revolt_models::v0;
|
use revolt_models::v0;
|
||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
@@ -26,13 +25,20 @@ pub async fn auth(
|
|||||||
return Err(create_error!(InvalidOperation));
|
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));
|
return Err(create_error!(InvalidOperation));
|
||||||
};
|
};
|
||||||
|
|
||||||
let config = config().await;
|
let config = config().await;
|
||||||
|
|
||||||
let code = match info.response_type {
|
let token = match info.response_type {
|
||||||
// implicit
|
// implicit
|
||||||
v0::OAuth2ResponseType::Code => {
|
v0::OAuth2ResponseType::Code => {
|
||||||
if info.state.is_some() || info.code_challenge.is_some() || info.code_challenge_method.is_some() {
|
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))
|
return Err(create_error!(InvalidOperation))
|
||||||
};
|
};
|
||||||
|
|
||||||
let code = oauth2::encode_token(
|
let token = oauth2::encode_token(
|
||||||
&config.api.security.token_secret,
|
&config.api.security.token_secret,
|
||||||
oauth2::TokenType::Access,
|
oauth2::TokenType::Access,
|
||||||
user.id.clone(),
|
user.id.clone(),
|
||||||
@@ -78,25 +84,15 @@ pub async fn auth(
|
|||||||
.map_err(|_| create_error!(InternalError))?;
|
.map_err(|_| create_error!(InternalError))?;
|
||||||
|
|
||||||
if let Some(code_challenge) = info.code_challenge.as_ref() {
|
if let Some(code_challenge) = info.code_challenge.as_ref() {
|
||||||
let mut conn = redis_kiss::get_connection()
|
oauth2::add_code_challange(&token, code_challenge).await?;
|
||||||
.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))?;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 }))
|
Ok(Json(v0::OAuth2AuthorizeAuthResponse { redirect_uri }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ pub async fn info(
|
|||||||
|
|
||||||
Ok(Json(v0::OAuth2AuthorizeInfoResponse {
|
Ok(Json(v0::OAuth2AuthorizeInfoResponse {
|
||||||
bot: public_bot,
|
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()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user