improvements

This commit is contained in:
Mateusz Gruszczyński
2026-08-05 09:53:28 +02:00
parent 248f4a3977
commit a9911da9a3
42 changed files with 1667 additions and 728 deletions
+55 -44
View File
@@ -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, &note).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, &note_slug)
.await?
.ok_or_else(ApiError::not_found_note)?;
let creator_can_manage_authorship = note_creator_is_requester(&state, &headers, &note).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, &note).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;