improvements
This commit is contained in:
+2
-43
@@ -620,46 +620,5 @@ fn sanitize_filename(value: &str) -> String {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{content_references_file, content_references_stored_file, is_safe_inline_image_mime};
|
||||
|
||||
#[test]
|
||||
fn only_raster_images_are_inline() {
|
||||
assert!(is_safe_inline_image_mime("image/png"));
|
||||
assert!(is_safe_inline_image_mime("image/jpeg"));
|
||||
assert!(!is_safe_inline_image_mime("image/svg+xml"));
|
||||
assert!(!is_safe_inline_image_mime("text/html"));
|
||||
assert!(!is_safe_inline_image_mime("application/xml"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extended_image_alias_is_still_attached() {
|
||||
assert!(content_references_file(
|
||||
"[image=photo.jpg,Photo,a=left,size=640x400]",
|
||||
"photo.jpg",
|
||||
"/f/token/photo.jpg",
|
||||
));
|
||||
assert!(content_references_file(
|
||||
"[file=report.pdf,Quarterly report]",
|
||||
"report.pdf",
|
||||
"/f/token/report.pdf",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_references_survive_origin_changes() {
|
||||
let stored = "/f/token/image.png";
|
||||
assert!(content_references_stored_file(
|
||||
"",
|
||||
"image.png",
|
||||
stored,
|
||||
None,
|
||||
));
|
||||
assert!(content_references_stored_file(
|
||||
"",
|
||||
"image.png",
|
||||
"https://old-files.example.com/f/token/image.png",
|
||||
Some("https://new-files.example.com"),
|
||||
));
|
||||
}
|
||||
}
|
||||
#[path = "../tests/api_files.rs"]
|
||||
mod tests;
|
||||
|
||||
+55
-44
@@ -89,14 +89,35 @@ fn requester_guest_id(headers: &HeaderMap) -> Option<&str> {
|
||||
})
|
||||
}
|
||||
|
||||
fn guest_owner_is_requester(headers: &HeaderMap, owner_guest_id: Option<&str>) -> bool {
|
||||
owner_guest_id
|
||||
.zip(requester_guest_id(headers))
|
||||
.is_some_and(|(owner_guest_id, requester_guest_id)| owner_guest_id == requester_guest_id)
|
||||
}
|
||||
|
||||
fn can_set_resource_password(
|
||||
password_protected: bool,
|
||||
account_owner: bool,
|
||||
guest_owner: bool,
|
||||
) -> bool {
|
||||
!password_protected && (account_owner || guest_owner)
|
||||
}
|
||||
|
||||
fn can_manage_resource_settings(
|
||||
account_owner: bool,
|
||||
guest_owner: bool,
|
||||
password_write_access: bool,
|
||||
) -> bool {
|
||||
account_owner || guest_owner || password_write_access
|
||||
}
|
||||
|
||||
async fn note_creator_is_requester(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
note: &db::Note,
|
||||
) -> Result<bool, ApiError> {
|
||||
if let Some(owner_guest_id) = note.created_by_guest_id.as_deref() {
|
||||
return Ok(requester_guest_id(headers)
|
||||
.is_some_and(|requester_guest_id| requester_guest_id == owner_guest_id));
|
||||
return Ok(guest_owner_is_requester(headers, Some(owner_guest_id)));
|
||||
}
|
||||
let Some(user) = session_user(state, headers).await? else {
|
||||
return Ok(false);
|
||||
@@ -108,18 +129,11 @@ async fn note_creator_is_requester(
|
||||
}
|
||||
|
||||
fn pad_creator_is_requester(headers: &HeaderMap, pad: &db::Pad) -> bool {
|
||||
pad.created_by_guest_id
|
||||
.as_deref()
|
||||
.zip(requester_guest_id(headers))
|
||||
.is_some_and(|(owner_guest_id, requester_guest_id)| owner_guest_id == requester_guest_id)
|
||||
guest_owner_is_requester(headers, pad.created_by_guest_id.as_deref())
|
||||
}
|
||||
|
||||
fn workspace_creator_is_requester(headers: &HeaderMap, workspace: &db::Workspace) -> bool {
|
||||
workspace
|
||||
.created_by_guest_id
|
||||
.as_deref()
|
||||
.zip(requester_guest_id(headers))
|
||||
.is_some_and(|(owner_guest_id, requester_guest_id)| owner_guest_id == requester_guest_id)
|
||||
guest_owner_is_requester(headers, workspace.created_by_guest_id.as_deref())
|
||||
}
|
||||
|
||||
async fn can_set_workspace_password(
|
||||
@@ -127,9 +141,6 @@ async fn can_set_workspace_password(
|
||||
headers: &HeaderMap,
|
||||
workspace: &db::Workspace,
|
||||
) -> bool {
|
||||
if workspace.password_hash.is_some() {
|
||||
return false;
|
||||
}
|
||||
let account_owner = crate::auth::is_resource_owner(
|
||||
state,
|
||||
"workspace",
|
||||
@@ -138,7 +149,11 @@ async fn can_set_workspace_password(
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
account_owner || workspace_creator_is_requester(headers, workspace)
|
||||
can_set_resource_password(
|
||||
workspace.password_hash.is_some(),
|
||||
account_owner,
|
||||
workspace_creator_is_requester(headers, workspace),
|
||||
)
|
||||
}
|
||||
|
||||
async fn has_write_permission(
|
||||
@@ -314,6 +329,8 @@ pub struct CreateNoteRequest {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SetWorkspacePasswordRequest {
|
||||
password: String,
|
||||
#[serde(default)]
|
||||
client_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -685,9 +702,14 @@ pub async fn set_workspace_password(
|
||||
"Only the workspace owner can set its password.",
|
||||
));
|
||||
}
|
||||
let except_client_id =
|
||||
crate::websocket::clean_collaboration_client_id(payload.client_id);
|
||||
let password = validate_password(Some(payload.password.as_str()))?
|
||||
.ok_or_else(|| ApiError::bad_request("Password is required."))?;
|
||||
db::set_workspace_password(&state.db, &workspace_slug, password).await?;
|
||||
state
|
||||
.notify_workspace_password_required(&workspace_slug, except_client_id)
|
||||
.await;
|
||||
Ok(Json(serde_json::json!({"ok": true, "protected": true})))
|
||||
}
|
||||
|
||||
@@ -1021,7 +1043,8 @@ pub async fn note_info(
|
||||
let note_owner = note_creator_is_requester(&state, &headers, ¬e).await?;
|
||||
let password_write_access =
|
||||
has_password_write_access(&state, &headers, "workspace", &workspace_slug).await?;
|
||||
let can_manage_authorship = workspace_owner || note_owner || password_write_access;
|
||||
let can_manage_authorship =
|
||||
can_manage_resource_settings(workspace_owner, note_owner, password_write_access);
|
||||
let can_delete_files = can_manage_authorship;
|
||||
let upload_max_size_bytes =
|
||||
resource_upload_limit(&state, &headers, "workspace", &workspace_slug).await?;
|
||||
@@ -1066,8 +1089,11 @@ pub async fn note_info(
|
||||
personal_editor_settings,
|
||||
can_save_editor_settings,
|
||||
can_manage_authorship,
|
||||
can_set_password: workspace.password_hash.is_none()
|
||||
&& (workspace_owner || workspace_guest_owner),
|
||||
can_set_password: can_set_resource_password(
|
||||
workspace.password_hash.is_some(),
|
||||
workspace_owner,
|
||||
workspace_guest_owner,
|
||||
),
|
||||
files: markdown_file_references(&state, None, Some(note.id), None).await?,
|
||||
}))
|
||||
}
|
||||
@@ -1084,8 +1110,11 @@ pub async fn set_note_editor_settings(
|
||||
let note = db::find_note(&state.db, workspace.id, ¬e_slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
let creator_can_manage_authorship = note_creator_is_requester(&state, &headers, ¬e).await?
|
||||
|| has_password_write_access(&state, &headers, "workspace", &workspace_slug).await?;
|
||||
let creator_can_manage_authorship = can_manage_resource_settings(
|
||||
false,
|
||||
note_creator_is_requester(&state, &headers, ¬e).await?,
|
||||
has_password_write_access(&state, &headers, "workspace", &workspace_slug).await?,
|
||||
);
|
||||
save_editor_settings(
|
||||
&state,
|
||||
&headers,
|
||||
@@ -1255,28 +1284,6 @@ fn permission_level(permission: Option<&str>) -> AccessLevel {
|
||||
}
|
||||
}
|
||||
|
||||
async fn anonymous_access_token_valid(
|
||||
state: &SharedState,
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
token: Option<&str>,
|
||||
) -> Result<bool, ApiError> {
|
||||
let Some(token) = token.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return Ok(false);
|
||||
};
|
||||
let count: i64 = sqlx::query_scalar(queries::get(
|
||||
state.db.kind(),
|
||||
queries::RESOURCE_ACCESS_TOKENS_VALID_COUNT,
|
||||
))
|
||||
.bind(access_tokens::hash_access_token(token))
|
||||
.bind(kind)
|
||||
.bind(slug)
|
||||
.bind(Utc::now().to_rfc3339())
|
||||
.fetch_one(state.db.pool())
|
||||
.await?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
async fn has_password_write_access(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
@@ -1287,7 +1294,7 @@ async fn has_password_write_access(
|
||||
crate::security::resource_token(headers, kind, slug),
|
||||
authorization_token(headers),
|
||||
] {
|
||||
if anonymous_access_token_valid(state, kind, slug, token).await? {
|
||||
if verify_password_access_token(state, kind, slug, token).await? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
@@ -1307,7 +1314,7 @@ async fn external_token_access_level(
|
||||
if level != AccessLevel::None {
|
||||
return Ok(level);
|
||||
}
|
||||
if anonymous_access_token_valid(state, kind, slug, token).await? {
|
||||
if verify_password_access_token(state, kind, slug, token).await? {
|
||||
// A server-issued token created after a correct resource password
|
||||
// retains the historical read/write semantics of password access.
|
||||
return Ok(AccessLevel::Write);
|
||||
@@ -1612,3 +1619,7 @@ async fn unique_note_slug(
|
||||
}
|
||||
Err(ApiError::internal("Failed to create a unique address"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../tests/api.rs"]
|
||||
mod guest_resource_access_tests;
|
||||
|
||||
+20
-5
@@ -21,6 +21,8 @@ pub struct CreatePadRequest {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SetPadPasswordRequest {
|
||||
password: String,
|
||||
#[serde(default)]
|
||||
client_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -135,7 +137,8 @@ pub async fn pad_info(
|
||||
.unwrap_or(false);
|
||||
let guest_owner = pad_creator_is_requester(&headers, &pad);
|
||||
let password_write_access = has_password_write_access(&state, &headers, "pad", &slug).await?;
|
||||
let can_manage_authorship = account_owner || guest_owner || password_write_access;
|
||||
let can_manage_authorship =
|
||||
can_manage_resource_settings(account_owner, guest_owner, password_write_access);
|
||||
let upload_max_size_bytes = resource_upload_limit(&state, &headers, "pad", &slug).await?;
|
||||
let can_upload_files = upload_max_size_bytes.is_some();
|
||||
let can_save_editor_settings = (personal_editor_settings || can_manage_authorship)
|
||||
@@ -174,7 +177,11 @@ pub async fn pad_info(
|
||||
personal_editor_settings,
|
||||
can_save_editor_settings,
|
||||
can_manage_authorship,
|
||||
can_set_password: pad.password_hash.is_none() && (account_owner || guest_owner),
|
||||
can_set_password: can_set_resource_password(
|
||||
pad.password_hash.is_some(),
|
||||
account_owner,
|
||||
guest_owner,
|
||||
),
|
||||
files: markdown_file_references(&state, Some(pad.id), None, None).await?,
|
||||
}))
|
||||
}
|
||||
@@ -195,12 +202,17 @@ pub async fn set_pad_password(
|
||||
&state, "pad", &slug, user_session_token(&headers),
|
||||
).await.unwrap_or(false);
|
||||
let guest_owner = pad_creator_is_requester(&headers, &pad);
|
||||
if !account_owner && !guest_owner {
|
||||
if !can_set_resource_password(pad.password_hash.is_some(), account_owner, guest_owner) {
|
||||
return Err(ApiError::forbidden("Only the note owner can set its password."));
|
||||
}
|
||||
let except_client_id =
|
||||
crate::websocket::clean_collaboration_client_id(payload.client_id);
|
||||
let password = validate_password(Some(payload.password.as_str()))?
|
||||
.ok_or_else(|| ApiError::bad_request("Password is required."))?;
|
||||
db::set_pad_password(&state.db, &slug, password).await?;
|
||||
state
|
||||
.notify_pad_password_required(&slug, except_client_id)
|
||||
.await;
|
||||
Ok(Json(serde_json::json!({"ok": true, "protected": true})))
|
||||
}
|
||||
|
||||
@@ -213,8 +225,11 @@ pub async fn set_pad_editor_settings(
|
||||
let pad = db::find_pad(&state.db, &slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
let creator_can_manage_authorship = pad_creator_is_requester(&headers, &pad)
|
||||
|| has_password_write_access(&state, &headers, "pad", &slug).await?;
|
||||
let creator_can_manage_authorship = can_manage_resource_settings(
|
||||
false,
|
||||
pad_creator_is_requester(&headers, &pad),
|
||||
has_password_write_access(&state, &headers, "pad", &slug).await?,
|
||||
);
|
||||
save_editor_settings(
|
||||
&state,
|
||||
&headers,
|
||||
|
||||
Reference in New Issue
Block a user