V2 removal and _internal rerouting (#770)

* deleteed v3 exclusive routes

* moved routes around

* fixed linkage that movement broke

* initial merge errors

* fixes
This commit is contained in:
Wyatt Verchere
2023-12-01 10:02:11 -08:00
committed by GitHub
parent 4bbc57b0dc
commit 2d92b08404
25 changed files with 93 additions and 899 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,12 @@
pub mod checks;
pub mod email;
pub mod flows;
pub mod oauth;
pub mod pats;
pub mod session;
mod templates;
pub mod templates;
pub mod validate;
pub use checks::{
filter_authorized_projects, filter_authorized_versions, is_authorized, is_authorized_version,
};
use serde::{Deserialize, Serialize};
// pub use pat::{generate_pat, PersonalAccessToken};
pub use validate::{check_is_moderator_from_headers, get_user_from_headers};
@@ -98,3 +96,16 @@ impl AuthenticationError {
}
}
}
#[derive(Serialize, Deserialize, Default, Eq, PartialEq, Clone, Copy, Debug)]
#[serde(rename_all = "lowercase")]
pub enum AuthProvider {
#[default]
GitHub,
Discord,
Microsoft,
GitLab,
Google,
Steam,
PayPal,
}

View File

@@ -1,282 +0,0 @@
use crate::database;
use crate::database::models::generate_pat_id;
use crate::auth::get_user_from_headers;
use crate::routes::ApiError;
use crate::database::redis::RedisPool;
use actix_web::web::{self, Data};
use actix_web::{delete, get, patch, post, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
use rand::distributions::Alphanumeric;
use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use crate::models::pats::{PersonalAccessToken, Scopes};
use crate::queue::session::AuthQueue;
use crate::util::validate::validation_errors_to_string;
use serde::Deserialize;
use sqlx::postgres::PgPool;
use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get_pats);
cfg.service(create_pat);
cfg.service(edit_pat);
cfg.service(delete_pat);
}
#[get("pat")]
pub async fn get_pats(
req: HttpRequest,
pool: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PAT_READ]),
)
.await?
.1;
let pat_ids = database::models::pat_item::PersonalAccessToken::get_user_pats(
user.id.into(),
&**pool,
&redis,
)
.await?;
let pats =
database::models::pat_item::PersonalAccessToken::get_many_ids(&pat_ids, &**pool, &redis)
.await?;
Ok(HttpResponse::Ok().json(
pats.into_iter()
.map(|x| PersonalAccessToken::from(x, false))
.collect::<Vec<_>>(),
))
}
#[derive(Deserialize, Validate)]
pub struct NewPersonalAccessToken {
pub scopes: Scopes,
#[validate(length(min = 3, max = 255))]
pub name: String,
pub expires: DateTime<Utc>,
}
#[post("pat")]
pub async fn create_pat(
req: HttpRequest,
info: web::Json<NewPersonalAccessToken>,
pool: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
info.0
.validate()
.map_err(|err| ApiError::InvalidInput(validation_errors_to_string(err, None)))?;
if info.scopes.is_restricted() {
return Err(ApiError::InvalidInput(
"Invalid scopes requested!".to_string(),
));
}
if info.expires < Utc::now() {
return Err(ApiError::InvalidInput(
"Expire date must be in the future!".to_string(),
));
}
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PAT_CREATE]),
)
.await?
.1;
let mut transaction = pool.begin().await?;
let id = generate_pat_id(&mut transaction).await?;
let token = ChaCha20Rng::from_entropy()
.sample_iter(&Alphanumeric)
.take(60)
.map(char::from)
.collect::<String>();
let token = format!("mrp_{}", token);
let name = info.name.clone();
database::models::pat_item::PersonalAccessToken {
id,
name: name.clone(),
access_token: token.clone(),
scopes: info.scopes,
user_id: user.id.into(),
created: Utc::now(),
expires: info.expires,
last_used: None,
}
.insert(&mut transaction)
.await?;
database::models::pat_item::PersonalAccessToken::clear_cache(
vec![(None, None, Some(user.id.into()))],
&redis,
)
.await?;
transaction.commit().await?;
Ok(HttpResponse::Ok().json(PersonalAccessToken {
id: id.into(),
name,
access_token: Some(token),
scopes: info.scopes,
user_id: user.id,
created: Utc::now(),
expires: info.expires,
last_used: None,
}))
}
#[derive(Deserialize, Validate)]
pub struct ModifyPersonalAccessToken {
pub scopes: Option<Scopes>,
#[validate(length(min = 3, max = 255))]
pub name: Option<String>,
pub expires: Option<DateTime<Utc>>,
}
#[patch("pat/{id}")]
pub async fn edit_pat(
req: HttpRequest,
id: web::Path<(String,)>,
info: web::Json<ModifyPersonalAccessToken>,
pool: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PAT_WRITE]),
)
.await?
.1;
let id = id.into_inner().0;
let pat = database::models::pat_item::PersonalAccessToken::get(&id, &**pool, &redis).await?;
if let Some(pat) = pat {
if pat.user_id == user.id.into() {
let mut transaction = pool.begin().await?;
if let Some(scopes) = &info.scopes {
if scopes.is_restricted() {
return Err(ApiError::InvalidInput(
"Invalid scopes requested!".to_string(),
));
}
sqlx::query!(
"
UPDATE pats
SET scopes = $1
WHERE id = $2
",
scopes.bits() as i64,
pat.id.0
)
.execute(&mut *transaction)
.await?;
}
if let Some(name) = &info.name {
sqlx::query!(
"
UPDATE pats
SET name = $1
WHERE id = $2
",
name,
pat.id.0
)
.execute(&mut *transaction)
.await?;
}
if let Some(expires) = &info.expires {
if expires < &Utc::now() {
return Err(ApiError::InvalidInput(
"Expire date must be in the future!".to_string(),
));
}
sqlx::query!(
"
UPDATE pats
SET expires = $1
WHERE id = $2
",
expires,
pat.id.0
)
.execute(&mut *transaction)
.await?;
}
database::models::pat_item::PersonalAccessToken::clear_cache(
vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))],
&redis,
)
.await?;
transaction.commit().await?;
}
}
Ok(HttpResponse::NoContent().finish())
}
#[delete("pat/{id}")]
pub async fn delete_pat(
req: HttpRequest,
id: web::Path<(String,)>,
pool: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PAT_DELETE]),
)
.await?
.1;
let id = id.into_inner().0;
let pat = database::models::pat_item::PersonalAccessToken::get(&id, &**pool, &redis).await?;
if let Some(pat) = pat {
if pat.user_id == user.id.into() {
let mut transaction = pool.begin().await?;
database::models::pat_item::PersonalAccessToken::remove(pat.id, &mut transaction)
.await?;
database::models::pat_item::PersonalAccessToken::clear_cache(
vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))],
&redis,
)
.await?;
transaction.commit().await?;
}
}
Ok(HttpResponse::NoContent().finish())
}

View File

@@ -1,253 +0,0 @@
use crate::auth::{get_user_from_headers, AuthenticationError};
use crate::database::models::session_item::Session as DBSession;
use crate::database::models::session_item::SessionBuilder;
use crate::database::models::UserId;
use crate::database::redis::RedisPool;
use crate::models::pats::Scopes;
use crate::models::sessions::Session;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::util::env::parse_var;
use actix_web::http::header::AUTHORIZATION;
use actix_web::web::{scope, Data, ServiceConfig};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use chrono::Utc;
use rand::distributions::Alphanumeric;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use sqlx::PgPool;
use woothee::parser::Parser;
pub fn config(cfg: &mut ServiceConfig) {
cfg.service(
scope("session")
.service(list)
.service(delete)
.service(refresh),
);
}
pub struct SessionMetadata {
pub city: Option<String>,
pub country: Option<String>,
pub ip: String,
pub os: Option<String>,
pub platform: Option<String>,
pub user_agent: String,
}
pub async fn get_session_metadata(
req: &HttpRequest,
) -> Result<SessionMetadata, AuthenticationError> {
let conn_info = req.connection_info().clone();
let ip_addr = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) {
if let Some(header) = req.headers().get("CF-Connecting-IP") {
header.to_str().ok()
} else {
conn_info.peer_addr()
}
} else {
conn_info.peer_addr()
};
let country = req
.headers()
.get("cf-ipcountry")
.and_then(|x| x.to_str().ok());
let city = req.headers().get("cf-ipcity").and_then(|x| x.to_str().ok());
let user_agent = req
.headers()
.get("user-agent")
.and_then(|x| x.to_str().ok())
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
let parser = Parser::new();
let info = parser.parse(user_agent);
let os = if let Some(info) = info {
Some((info.os, info.name))
} else {
None
};
Ok(SessionMetadata {
os: os.map(|x| x.0.to_string()),
platform: os.map(|x| x.1.to_string()),
city: city.map(|x| x.to_string()),
country: country.map(|x| x.to_string()),
ip: ip_addr
.ok_or_else(|| AuthenticationError::InvalidCredentials)?
.to_string(),
user_agent: user_agent.to_string(),
})
}
pub async fn issue_session(
req: HttpRequest,
user_id: UserId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<DBSession, AuthenticationError> {
let metadata = get_session_metadata(&req).await?;
let session = ChaCha20Rng::from_entropy()
.sample_iter(&Alphanumeric)
.take(60)
.map(char::from)
.collect::<String>();
let session = format!("mra_{session}");
let id = SessionBuilder {
session,
user_id,
os: metadata.os,
platform: metadata.platform,
city: metadata.city,
country: metadata.country,
ip: metadata.ip,
user_agent: metadata.user_agent,
}
.insert(transaction)
.await?;
let session = DBSession::get_id(id, &mut **transaction, redis)
.await?
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
DBSession::clear_cache(
vec![(
Some(session.id),
Some(session.session.clone()),
Some(session.user_id),
)],
redis,
)
.await?;
Ok(session)
}
#[get("list")]
pub async fn list(
req: HttpRequest,
pool: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::SESSION_READ]),
)
.await?
.1;
let session = req
.headers()
.get(AUTHORIZATION)
.and_then(|x| x.to_str().ok())
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
let session_ids = DBSession::get_user_sessions(current_user.id.into(), &**pool, &redis).await?;
let sessions = DBSession::get_many_ids(&session_ids, &**pool, &redis)
.await?
.into_iter()
.filter(|x| x.expires > Utc::now())
.map(|x| Session::from(x, false, Some(session)))
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(sessions))
}
#[delete("{id}")]
pub async fn delete(
info: web::Path<(String,)>,
req: HttpRequest,
pool: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::SESSION_DELETE]),
)
.await?
.1;
let session = DBSession::get(info.into_inner().0, &**pool, &redis).await?;
if let Some(session) = session {
if session.user_id == current_user.id.into() {
let mut transaction = pool.begin().await?;
DBSession::remove(session.id, &mut transaction).await?;
DBSession::clear_cache(
vec![(
Some(session.id),
Some(session.session),
Some(session.user_id),
)],
&redis,
)
.await?;
transaction.commit().await?;
}
}
Ok(HttpResponse::NoContent().body(""))
}
#[post("refresh")]
pub async fn refresh(
req: HttpRequest,
pool: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue, None)
.await?
.1;
let session = req
.headers()
.get(AUTHORIZATION)
.and_then(|x| x.to_str().ok())
.ok_or_else(|| ApiError::Authentication(AuthenticationError::InvalidCredentials))?;
let session = DBSession::get(session, &**pool, &redis).await?;
if let Some(session) = session {
if current_user.id != session.user_id.into() || session.refresh_expires < Utc::now() {
return Err(ApiError::Authentication(
AuthenticationError::InvalidCredentials,
));
}
let mut transaction = pool.begin().await?;
DBSession::remove(session.id, &mut transaction).await?;
let new_session = issue_session(req, session.user_id, &mut transaction, &redis).await?;
DBSession::clear_cache(
vec![(
Some(session.id),
Some(session.session),
Some(session.user_id),
)],
&redis,
)
.await?;
transaction.commit().await?;
Ok(HttpResponse::Ok().json(Session::from(new_session, true, None)))
} else {
Err(ApiError::Authentication(
AuthenticationError::InvalidCredentials,
))
}
}

View File

@@ -1,11 +1,11 @@
use crate::auth::flows::AuthProvider;
use crate::auth::session::get_session_metadata;
use super::AuthProvider;
use crate::auth::AuthenticationError;
use crate::database::models::user_item;
use crate::database::redis::RedisPool;
use crate::models::pats::Scopes;
use crate::models::users::{Role, User, UserId, UserPayoutData};
use crate::queue::session::AuthQueue;
use crate::routes::internal::session::get_session_metadata;
use actix_web::HttpRequest;
use chrono::Utc;
use reqwest::header::{HeaderValue, AUTHORIZATION};