OAuth 2.0 Authorization Server [MOD-559] (#733)

* WIP end-of-day push

* Authorize endpoint, accept endpoints, DB stuff for oauth clients, their redirects, and client authorizations

* OAuth Client create route

* Get user clients

* Client delete

* Edit oauth client

* Include redirects in edit client route

* Database stuff for tokens

* Reorg oauth stuff out of auth/flows and into its own module

* Impl OAuth get access token endpoint

* Accept oauth access tokens as auth and update through AuthQueue

* User OAuth authorization management routes

* Forgot to actually add the routes lol

* Bit o cleanup

* Happy path test for OAuth and minor fixes for things it found

* Add dummy data oauth client (and detect/handle dummy data version changes)

* More tests

* Another test

* More tests and reject endpoint

* Test oauth client and authorization management routes

* cargo sqlx prepare

* dead code warning

* Auto clippy fixes

* Uri refactoring

* minor name improvement

* Don't compile-time check the test sqlx queries

* Trying to fix db concurrency problem to get tests to pass

* Try fix from test PR

* Fixes for updated sqlx

* Prevent restricted scopes from being requested or issued

* Get OAuth client(s)

* Remove joined oauth client info from authorization returns

* Add default conversion to OAuthError::error so we can use ?

* Rework routes

* Consolidate scopes into SESSION_ACCESS

* Cargo sqlx prepare

* Parse to OAuthClientId automatically through serde and actix

* Cargo clippy

* Remove validation requiring 1 redirect URI on oauth client creation

* Use serde(flatten) on OAuthClientCreationResult
This commit is contained in:
Jackson Kruger
2023-10-30 11:14:38 -05:00
committed by GitHub
parent 8803e11945
commit 6cfd4637db
54 changed files with 3658 additions and 135 deletions

View File

@@ -1,8 +1,11 @@
#![allow(dead_code)]
use actix_http::StatusCode;
use actix_web::test::{self, TestRequest};
use labrinth::{
models::projects::Project,
models::{organizations::Organization, pats::Scopes, projects::Version},
models::{
oauth_clients::OAuthClient, organizations::Organization, pats::Scopes, projects::Version,
},
};
use serde_json::json;
use sqlx::Executor;
@@ -11,11 +14,14 @@ use crate::common::{actix::AppendsMultipart, database::USER_USER_PAT};
use super::{
actix::{MultipartSegment, MultipartSegmentData},
asserts::assert_status,
database::USER_USER_ID,
environment::TestEnvironment,
get_json_val_str,
request_data::get_public_project_creation_data,
};
pub const DUMMY_DATA_UPDATE: i64 = 1;
pub const DUMMY_DATA_UPDATE: i64 = 3;
#[allow(dead_code)]
pub const DUMMY_CATEGORIES: &[&str] = &[
@@ -28,6 +34,8 @@ pub const DUMMY_CATEGORIES: &[&str] = &[
"optimization",
];
pub const DUMMY_OAUTH_CLIENT_ALPHA_SECRET: &str = "abcdefghijklmnopqrstuvwxyz";
#[allow(dead_code)]
pub enum DummyJarFile {
DummyProjectAlpha,
@@ -43,16 +51,80 @@ pub enum DummyImage {
#[derive(Clone)]
pub struct DummyData {
/// Alpha project:
/// This is a dummy project created by USER user.
/// It's approved, listed, and visible to the public.
pub project_alpha: DummyProjectAlpha,
/// Beta project:
/// This is a dummy project created by USER user.
/// It's not approved, unlisted, and not visible to the public.
pub project_beta: DummyProjectBeta,
/// Zeta organization:
/// This is a dummy organization created by USER user.
/// There are no projects in it.
pub organization_zeta: DummyOrganizationZeta,
/// Alpha OAuth Client:
/// This is a dummy OAuth client created by USER user.
///
/// All scopes are included in its max scopes
///
/// It has one valid redirect URI
pub oauth_client_alpha: DummyOAuthClientAlpha,
}
impl DummyData {
pub fn new(
project_alpha: Project,
project_alpha_version: Version,
project_beta: Project,
project_beta_version: Version,
organization_zeta: Organization,
oauth_client_alpha: OAuthClient,
) -> Self {
DummyData {
project_alpha: DummyProjectAlpha {
team_id: project_alpha.team.to_string(),
project_id: project_alpha.id.to_string(),
project_slug: project_alpha.slug.unwrap(),
version_id: project_alpha_version.id.to_string(),
thread_id: project_alpha.thread_id.to_string(),
file_hash: project_alpha_version.files[0].hashes["sha1"].clone(),
},
project_beta: DummyProjectBeta {
team_id: project_beta.team.to_string(),
project_id: project_beta.id.to_string(),
project_slug: project_beta.slug.unwrap(),
version_id: project_beta_version.id.to_string(),
thread_id: project_beta.thread_id.to_string(),
file_hash: project_beta_version.files[0].hashes["sha1"].clone(),
},
organization_zeta: DummyOrganizationZeta {
organization_id: organization_zeta.id.to_string(),
team_id: organization_zeta.team_id.to_string(),
organization_title: organization_zeta.title,
},
oauth_client_alpha: DummyOAuthClientAlpha {
client_id: get_json_val_str(oauth_client_alpha.id),
client_secret: DUMMY_OAUTH_CLIENT_ALPHA_SECRET.to_string(),
valid_redirect_uri: oauth_client_alpha
.redirect_uris
.first()
.unwrap()
.uri
.clone(),
},
}
}
}
#[derive(Clone)]
pub struct DummyProjectAlpha {
// Alpha project:
// This is a dummy project created by USER user.
// It's approved, listed, and visible to the public.
pub project_id: String,
pub project_slug: String,
pub version_id: String,
@@ -63,9 +135,6 @@ pub struct DummyProjectAlpha {
#[derive(Clone)]
pub struct DummyProjectBeta {
// Beta project:
// This is a dummy project created by USER user.
// It's not approved, unlisted, and not visible to the public.
pub project_id: String,
pub project_slug: String,
pub version_id: String,
@@ -76,14 +145,18 @@ pub struct DummyProjectBeta {
#[derive(Clone)]
pub struct DummyOrganizationZeta {
// Zeta organization:
// This is a dummy organization created by USER user.
// There are no projects in it.
pub organization_id: String,
pub organization_title: String,
pub team_id: String,
}
#[derive(Clone)]
pub struct DummyOAuthClientAlpha {
pub client_id: String,
pub client_secret: String,
pub valid_redirect_uri: String,
}
pub async fn add_dummy_data(test_env: &TestEnvironment) -> DummyData {
// Adds basic dummy data to the database directly with sql (user, pats)
let pool = &test_env.db.pool.clone();
@@ -101,37 +174,22 @@ pub async fn add_dummy_data(test_env: &TestEnvironment) -> DummyData {
let zeta_organization = add_organization_zeta(test_env).await;
let oauth_client_alpha = get_oauth_client_alpha(test_env).await;
sqlx::query("INSERT INTO dummy_data (update_id) VALUES ($1)")
.bind(DUMMY_DATA_UPDATE)
.execute(pool)
.await
.unwrap();
DummyData {
project_alpha: DummyProjectAlpha {
team_id: alpha_project.team.to_string(),
project_id: alpha_project.id.to_string(),
project_slug: alpha_project.slug.unwrap(),
version_id: alpha_version.id.to_string(),
thread_id: alpha_project.thread_id.to_string(),
file_hash: alpha_version.files[0].hashes["sha1"].clone(),
},
project_beta: DummyProjectBeta {
team_id: beta_project.team.to_string(),
project_id: beta_project.id.to_string(),
project_slug: beta_project.slug.unwrap(),
version_id: beta_version.id.to_string(),
thread_id: beta_project.thread_id.to_string(),
file_hash: beta_version.files[0].hashes["sha1"].clone(),
},
organization_zeta: DummyOrganizationZeta {
organization_id: zeta_organization.id.to_string(),
team_id: zeta_organization.team_id.to_string(),
organization_title: zeta_organization.title,
},
}
DummyData::new(
alpha_project,
alpha_version,
beta_project,
beta_version,
zeta_organization,
oauth_client_alpha,
)
}
pub async fn get_dummy_data(test_env: &TestEnvironment) -> DummyData {
@@ -139,31 +197,17 @@ pub async fn get_dummy_data(test_env: &TestEnvironment) -> DummyData {
let (beta_project, beta_version) = get_project_beta(test_env).await;
let zeta_organization = get_organization_zeta(test_env).await;
DummyData {
project_alpha: DummyProjectAlpha {
team_id: alpha_project.team.to_string(),
project_id: alpha_project.id.to_string(),
project_slug: alpha_project.slug.unwrap(),
version_id: alpha_version.id.to_string(),
thread_id: alpha_project.thread_id.to_string(),
file_hash: alpha_version.files[0].hashes["sha1"].clone(),
},
project_beta: DummyProjectBeta {
team_id: beta_project.team.to_string(),
project_id: beta_project.id.to_string(),
project_slug: beta_project.slug.unwrap(),
version_id: beta_version.id.to_string(),
thread_id: beta_project.thread_id.to_string(),
file_hash: beta_version.files[0].hashes["sha1"].clone(),
},
let oauth_client_alpha = get_oauth_client_alpha(test_env).await;
organization_zeta: DummyOrganizationZeta {
organization_id: zeta_organization.id.to_string(),
team_id: zeta_organization.team_id.to_string(),
organization_title: zeta_organization.title,
},
}
DummyData::new(
alpha_project,
alpha_version,
beta_project,
beta_version,
zeta_organization,
oauth_client_alpha,
)
}
pub async fn add_project_alpha(test_env: &TestEnvironment) -> (Project, Version) {
@@ -282,6 +326,7 @@ pub async fn get_project_beta(test_env: &TestEnvironment) -> (Project, Version)
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
assert_status(&resp, StatusCode::OK);
let project: Project = test::read_body_json(resp).await;
// Get project's versions
@@ -290,6 +335,7 @@ pub async fn get_project_beta(test_env: &TestEnvironment) -> (Project, Version)
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
assert_status(&resp, StatusCode::OK);
let versions: Vec<Version> = test::read_body_json(resp).await;
let version = versions.into_iter().next().unwrap();
@@ -308,6 +354,14 @@ pub async fn get_organization_zeta(test_env: &TestEnvironment) -> Organization {
organization
}
pub async fn get_oauth_client_alpha(test_env: &TestEnvironment) -> OAuthClient {
let oauth_clients = test_env
.v3
.get_user_oauth_clients(USER_USER_ID, USER_USER_PAT)
.await;
oauth_clients.into_iter().next().unwrap()
}
impl DummyJarFile {
pub fn filename(&self) -> String {
match self {