Add fields to OAuth (#769)

* Add url and description fields to OAuthClient
model

* Add OAuth client icon editing and deleting
endpoints

* updated query data

* fix missed queries

* sqlx prep

* update with tests builds
This commit is contained in:
Carter 2023-11-25 20:48:51 -08:00 committed by GitHub
parent bad350e49b
commit 0efbbed5e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 228 additions and 11 deletions

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n clients.id as \"id!\",\n clients.name as \"name!\",\n clients.icon_url as \"icon_url?\",\n clients.max_scopes as \"max_scopes!\",\n clients.secret_hash as \"secret_hash!\",\n clients.created as \"created!\",\n clients.created_by as \"created_by!\",\n uris.uri_ids as \"uri_ids?\",\n uris.uri_vals as \"uri_vals?\"\n FROM oauth_clients clients\n LEFT JOIN (\n SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals\n FROM oauth_client_redirect_uris\n GROUP BY client_id\n ) uris ON clients.id = uris.client_id\n WHERE created_by = $1",
"query": "\n SELECT\n clients.id as \"id!\",\n clients.name as \"name!\",\n clients.icon_url as \"icon_url?\",\n clients.max_scopes as \"max_scopes!\",\n clients.secret_hash as \"secret_hash!\",\n clients.created as \"created!\",\n clients.created_by as \"created_by!\",\n clients.url as \"url?\",\n clients.description as \"description?\",\n uris.uri_ids as \"uri_ids?\",\n uris.uri_vals as \"uri_vals?\"\n FROM oauth_clients clients\n LEFT JOIN (\n SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals\n FROM oauth_client_redirect_uris\n GROUP BY client_id\n ) uris ON clients.id = uris.client_id\n WHERE created_by = $1",
"describe": {
"columns": [
{
@ -40,11 +40,21 @@
},
{
"ordinal": 7,
"name": "url?",
"type_info": "Text"
},
{
"ordinal": 8,
"name": "description?",
"type_info": "Text"
},
{
"ordinal": 9,
"name": "uri_ids?",
"type_info": "Int8Array"
},
{
"ordinal": 8,
"ordinal": 10,
"name": "uri_vals?",
"type_info": "TextArray"
}
@ -62,9 +72,11 @@
false,
false,
false,
true,
true,
null,
null
]
},
"hash": "8bfb350d4f539a110b05f42812ea2593a1556ef214f3bed519de6b6e21c7d477"
"hash": "467df7989a1f4f5f3bc3409a4060e89dcfdd5a5ab9c7a96dc4008e74e06be914"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE oauth_clients\n SET name = $1, icon_url = $2, max_scopes = $3\n WHERE (id = $4)\n ",
"query": "\n UPDATE oauth_clients\n SET name = $1, icon_url = $2, max_scopes = $3, url = $4, description = $5\n WHERE (id = $6)\n ",
"describe": {
"columns": [],
"parameters": {
@ -8,10 +8,12 @@
"Text",
"Text",
"Int8",
"Text",
"Text",
"Int8"
]
},
"nullable": []
},
"hash": "e8cc8895ebc8b1904a43e00f1e123f75ffdaebc76d67a5d35218fa9273d46d53"
"hash": "781f54f36b8713bd36f5f89c98f8c89e94cc7c54e0b7302c877586186bb128a2"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n clients.id as \"id!\",\n clients.name as \"name!\",\n clients.icon_url as \"icon_url?\",\n clients.max_scopes as \"max_scopes!\",\n clients.secret_hash as \"secret_hash!\",\n clients.created as \"created!\",\n clients.created_by as \"created_by!\",\n uris.uri_ids as \"uri_ids?\",\n uris.uri_vals as \"uri_vals?\"\n FROM oauth_clients clients\n LEFT JOIN (\n SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals\n FROM oauth_client_redirect_uris\n GROUP BY client_id\n ) uris ON clients.id = uris.client_id\n WHERE clients.id = ANY($1::bigint[])",
"query": "\n SELECT\n clients.id as \"id!\",\n clients.name as \"name!\",\n clients.icon_url as \"icon_url?\",\n clients.max_scopes as \"max_scopes!\",\n clients.secret_hash as \"secret_hash!\",\n clients.created as \"created!\",\n clients.created_by as \"created_by!\",\n clients.url as \"url?\",\n clients.description as \"description?\",\n uris.uri_ids as \"uri_ids?\",\n uris.uri_vals as \"uri_vals?\"\n FROM oauth_clients clients\n LEFT JOIN (\n SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals\n FROM oauth_client_redirect_uris\n GROUP BY client_id\n ) uris ON clients.id = uris.client_id\n WHERE clients.id = ANY($1::bigint[])",
"describe": {
"columns": [
{
@ -40,11 +40,21 @@
},
{
"ordinal": 7,
"name": "url?",
"type_info": "Text"
},
{
"ordinal": 8,
"name": "description?",
"type_info": "Text"
},
{
"ordinal": 9,
"name": "uri_ids?",
"type_info": "Int8Array"
},
{
"ordinal": 8,
"ordinal": 10,
"name": "uri_vals?",
"type_info": "TextArray"
}
@ -62,9 +72,11 @@
true,
true,
true,
true,
true,
null,
null
]
},
"hash": "fdfb2433a8e407d42cec1791d67549ab5c23306758168af38f955c06d251b0b7"
"hash": "93190af0fcfe2e795ed6b90eece7c567b8724509d1fdbd5cc75f516c1811c9d3"
}

View File

@ -0,0 +1,7 @@
-- Add migration script here
ALTER TABLE
oauth_clients
ADD
COLUMN url text NULL,
ADD
COLUMN description text NULL;

View File

@ -23,6 +23,8 @@ pub struct OAuthClient {
pub redirect_uris: Vec<OAuthRedirectUri>,
pub created: DateTime<Utc>,
pub created_by: UserId,
pub url: Option<String>,
pub description: Option<String>,
}
struct ClientQueryResult {
@ -33,6 +35,8 @@ struct ClientQueryResult {
secret_hash: String,
created: DateTime<Utc>,
created_by: i64,
url: Option<String>,
description: Option<String>,
uri_ids: Option<Vec<i64>>,
uri_vals: Option<Vec<String>>,
}
@ -53,6 +57,8 @@ macro_rules! select_clients_with_predicate {
clients.secret_hash as "secret_hash!",
clients.created as "created!",
clients.created_by as "created_by!",
clients.url as "url?",
clients.description as "description?",
uris.uri_ids as "uri_ids?",
uris.uri_vals as "uri_vals?"
FROM oauth_clients clients
@ -155,12 +161,14 @@ impl OAuthClient {
sqlx::query!(
"
UPDATE oauth_clients
SET name = $1, icon_url = $2, max_scopes = $3
WHERE (id = $4)
SET name = $1, icon_url = $2, max_scopes = $3, url = $4, description = $5
WHERE (id = $6)
",
self.name,
self.icon_url,
self.max_scopes.to_postgres(),
self.url,
self.description,
self.id.0,
)
.execute(exec)
@ -240,6 +248,8 @@ impl From<ClientQueryResult> for OAuthClient {
redirect_uris: redirects,
created: r.created,
created_by: UserId(r.created_by),
url: r.url,
description: r.description,
}
}
}

View File

@ -54,6 +54,13 @@ pub struct OAuthClient {
// The user that created (and thus controls) this client
pub created_by: UserId,
// When this client was created
pub created: DateTime<Utc>,
// (optional) Metadata about the client
pub url: Option<String>,
pub description: Option<String>,
}
#[derive(Deserialize, Serialize)]
@ -88,6 +95,9 @@ impl From<DBOAuthClient> for OAuthClient {
max_scopes: value.max_scopes,
redirect_uris: value.redirect_uris.into_iter().map(|r| r.into()).collect(),
created_by: value.created_by.into(),
created: value.created,
url: value.url,
description: value.description,
}
}
}

View File

@ -1,4 +1,4 @@
use std::{collections::HashSet, fmt::Display};
use std::{collections::HashSet, fmt::Display, sync::Arc};
use actix_web::{
delete, get, patch, post,
@ -16,7 +16,9 @@ use validator::Validate;
use super::ApiError;
use crate::{
auth::checks::ValidateAllAuthorized,
file_hosting::FileHost,
models::{ids::base62_impl::parse_base62, oauth_clients::DeleteOAuthClientQueryParam},
util::routes::read_from_payload,
};
use crate::{
auth::{checks::ValidateAuthorized, get_user_from_headers},
@ -50,6 +52,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.service(oauth_client_create)
.service(oauth_client_edit)
.service(oauth_client_delete)
.service(oauth_client_icon_edit)
.service(oauth_client_icon_delete)
.service(get_client)
.service(get_clients)
.service(get_user_oauth_authorizations),
@ -145,6 +149,15 @@ pub struct NewOAuthApp {
pub max_scopes: Scopes,
pub redirect_uris: Vec<String>,
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 255)
)]
pub url: Option<String>,
#[validate(length(max = 255))]
pub description: Option<String>,
}
#[post("app")]
@ -187,6 +200,8 @@ pub async fn oauth_client_create<'a>(
redirect_uris,
created: Utc::now(),
created_by: current_user.id.into(),
url: new_oauth_app.url.clone(),
description: new_oauth_app.description.clone(),
secret_hash: client_secret_hash,
};
client.clone().insert(&mut transaction).await?;
@ -248,6 +263,15 @@ pub struct OAuthClientEdit {
#[validate(length(min = 1))]
pub redirect_uris: Option<Vec<String>>,
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 255)
)]
pub url: Option<Option<String>>,
#[validate(length(max = 255))]
pub description: Option<Option<String>>,
}
#[patch("app/{id}")]
@ -289,6 +313,8 @@ pub async fn oauth_client_edit(
icon_url,
max_scopes,
redirect_uris,
url,
description,
} = client_updates.into_inner();
if let Some(name) = name {
updated_client.name = name;
@ -302,6 +328,14 @@ pub async fn oauth_client_edit(
updated_client.max_scopes = max_scopes;
}
if let Some(url) = url {
updated_client.url = url;
}
if let Some(description) = description {
updated_client.description = description;
}
let mut transaction = pool.begin().await?;
updated_client
.update_editable_fields(&mut *transaction)
@ -319,6 +353,130 @@ pub async fn oauth_client_edit(
}
}
#[derive(Serialize, Deserialize)]
pub struct Extension {
pub ext: String,
}
#[patch("app/{id}/icon")]
#[allow(clippy::too_many_arguments)]
pub async fn oauth_client_icon_edit(
web::Query(ext): web::Query<Extension>,
req: HttpRequest,
client_id: web::Path<ApiOAuthClientId>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::SESSION_ACCESS]),
)
.await?
.1;
let client = OAuthClient::get((*client_id).into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified client does not exist!".to_string())
})?;
client.validate_authorized(Some(&user))?;
if let Some(ref icon) = client.icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
}
}
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let upload_data = file_host
.upload_file(
content_type,
&format!("data/{}/{}.{}", client_id, hash, ext.ext),
bytes.freeze(),
)
.await?;
let mut transaction = pool.begin().await?;
let mut editable_client = client.clone();
editable_client.icon_url = Some(format!("{}/{}", cdn_url, upload_data.file_name));
editable_client
.update_editable_fields(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInput(format!(
"Invalid format for project icon: {}",
ext.ext
)))
}
}
#[delete("app/{id}/icon")]
pub async fn oauth_client_icon_delete(
req: HttpRequest,
client_id: web::Path<ApiOAuthClientId>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::SESSION_ACCESS]),
)
.await?
.1;
let client = OAuthClient::get((*client_id).into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified client does not exist!".to_string())
})?;
client.validate_authorized(Some(&user))?;
if let Some(ref icon) = client.icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
}
}
let mut transaction = pool.begin().await?;
let mut editable_client = client.clone();
editable_client.icon_url = None;
editable_client
.update_editable_fields(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
}
#[get("authorizations")]
pub async fn get_user_oauth_authorizations(
req: HttpRequest,

View File

@ -41,6 +41,8 @@ async fn can_create_edit_get_oauth_client() {
let client_id = get_json_val_str(creation_result.client.id);
let icon_url = Some("https://modrinth.com/icon".to_string());
let url = Some("https://modrinth.com".to_string());
let description = Some("test description".to_string());
let edited_redirect_uris = vec![
redirect_uris[0].clone(),
"https://modrinth.com/b".to_string(),
@ -50,6 +52,8 @@ async fn can_create_edit_get_oauth_client() {
icon_url: Some(icon_url.clone()),
max_scopes: None,
redirect_uris: Some(edited_redirect_uris.clone()),
url: Some(url.clone()),
description: Some(description.clone()),
};
let resp = env
.api
@ -63,6 +67,8 @@ async fn can_create_edit_get_oauth_client() {
.await;
assert_eq!(1, clients.len());
assert_eq!(icon_url, clients[0].icon_url);
assert_eq!(url, clients[0].url);
assert_eq!(description, clients[0].description);
assert_eq!(client_name, clients[0].name);
assert_eq!(2, clients[0].redirect_uris.len());
assert_eq!(edited_redirect_uris[0], clients[0].redirect_uris[0].uri);