fix2 tokens

This commit is contained in:
Mateusz Gruszczyński
2026-08-03 01:35:05 +02:00
parent 49dad1a5f4
commit e3ee6319b9
25 changed files with 1420 additions and 223 deletions
+118 -36
View File
@@ -119,23 +119,19 @@ async fn has_write_permission(
kind: &str,
slug: &str,
) -> Result<bool, ApiError> {
let resource = crate::security::resource_token(headers, kind, slug);
if external_token_access_level(state, kind, slug, resource).await? >= AccessLevel::Write {
return Ok(true);
}
let authorization = authorization_token(headers);
if authorization != resource
&& external_token_access_level(state, kind, slug, authorization).await?
>= AccessLevel::Write
if request_access_level(
state,
headers,
kind,
slug,
None,
crate::security::session_cookie_token(headers),
)
.await?
>= AccessLevel::Write
{
return Ok(true);
}
let session = crate::security::session_cookie_token(headers);
if session != resource && session != authorization {
if account_token_access_level(state, kind, slug, session).await? >= AccessLevel::Write {
return Ok(true);
}
}
match kind {
"workspace" => Ok(db::find_workspace(&state.db, slug)
.await?
@@ -275,6 +271,7 @@ pub struct WorkspaceInfo {
slug: String,
title: String,
protected: bool,
access_level: String,
created_at: String,
updated_at: String,
}
@@ -332,6 +329,7 @@ pub struct NoteInfo {
slug: String,
title: String,
protected: bool,
access_level: String,
note_protected: bool,
allow_public_task_updates: bool,
public_page_unprotected: bool,
@@ -570,7 +568,16 @@ pub async fn workspace_info(
workspace.is_private,
)
.await?;
Ok(Json(workspace_info_from(&workspace)))
let access_level = effective_header_access_level(
&state,
&headers,
"workspace",
&workspace.slug,
workspace.is_private,
workspace.password_hash.is_some(),
)
.await?;
Ok(Json(workspace_info_from(&workspace, access_level)))
}
pub async fn open_workspace(
@@ -636,8 +643,20 @@ pub async fn open_workspace(
let start = (page - 1) * per_page;
let notes = notes.into_iter().skip(start).take(per_page).collect();
let mut access_level = effective_header_access_level(
&state,
&headers,
"workspace",
&workspace.slug,
workspace.is_private,
workspace.password_hash.is_some(),
)
.await?;
if db::verify_workspace_password(&workspace, payload.password.as_deref()) {
access_level = AccessLevel::Write;
}
Ok(Json(WorkspaceOpenResponse {
workspace: workspace_info_from(&workspace),
workspace: workspace_info_from(&workspace, access_level),
notes,
pagination: ListPaginationMeta { page, per_page, total, total_pages },
}))
@@ -668,8 +687,9 @@ pub async fn create_note(
{
AccessLevel::Write
} else {
combined_token_access_level(
request_access_level(
&state,
&headers,
"workspace",
&workspace_slug,
resource_request_token(
@@ -845,6 +865,15 @@ pub async fn note_info(
workspace.is_private,
)
.await?;
let access_level = effective_header_access_level(
&state,
&headers,
"workspace",
&workspace.slug,
workspace.is_private,
workspace.password_hash.is_some(),
)
.await?;
let note = db::find_note(&state.db, workspace.id, &note_slug)
.await?
.ok_or_else(ApiError::not_found_note)?;
@@ -888,6 +917,7 @@ pub async fn note_info(
slug: note.slug,
title: note.title,
protected: workspace.password_hash.is_some(),
access_level: access_level_name(access_level).into(),
note_protected: note.protected,
allow_public_task_updates: db::note_public_task_updates(&state.db, note.id).await?,
public_page_unprotected: db::note_public_page_unprotected(&state.db, note.id).await?,
@@ -1001,8 +1031,9 @@ pub async fn restore(
{
AccessLevel::Write
} else {
combined_token_access_level(
request_access_level(
&state,
&headers,
"workspace",
&workspace_slug,
resource_request_token(
@@ -1105,7 +1136,7 @@ async fn external_token_access_level(
slug: &str,
token: Option<&str>,
) -> Result<AccessLevel, ApiError> {
let permission = crate::auth::share_link_permission(state, kind, slug, token)
let permission = crate::auth::share_access_permission(state, kind, slug, token)
.await
.map_err(|error| ApiError::forbidden(&error.message))?;
let level = permission_level(permission.as_deref());
@@ -1132,17 +1163,73 @@ async fn account_token_access_level(
Ok(permission_level(permission.as_deref()))
}
async fn combined_token_access_level(
async fn request_access_level(
state: &SharedState,
headers: &HeaderMap,
kind: &str,
slug: &str,
access_token: Option<&str>,
supplied_access_token: Option<&str>,
account_token: Option<&str>,
) -> Result<AccessLevel, ApiError> {
Ok(std::cmp::max(
external_token_access_level(state, kind, slug, access_token).await?,
account_token_access_level(state, kind, slug, account_token).await?,
))
let mut level = account_token_access_level(state, kind, slug, account_token).await?;
if level == AccessLevel::Write {
return Ok(level);
}
let mut checked_tokens = Vec::with_capacity(4);
for token in [
supplied_access_token,
crate::security::share_session_token(headers, kind, slug),
crate::security::resource_token(headers, kind, slug),
authorization_token(headers),
] {
let Some(token) = token.map(str::trim).filter(|value| !value.is_empty()) else {
continue;
};
if checked_tokens.contains(&token) {
continue;
}
checked_tokens.push(token);
level = std::cmp::max(
level,
external_token_access_level(state, kind, slug, Some(token)).await?,
);
if level == AccessLevel::Write {
break;
}
}
Ok(level)
}
fn access_level_name(level: AccessLevel) -> &'static str {
match level {
AccessLevel::None => "none",
AccessLevel::Read => "read",
AccessLevel::Write => "write",
}
}
async fn effective_header_access_level(
state: &SharedState,
headers: &HeaderMap,
kind: &str,
slug: &str,
is_private: i64,
password_protected: bool,
) -> Result<AccessLevel, ApiError> {
let mut level = request_access_level(
state,
headers,
kind,
slug,
None,
bearer_token(headers),
)
.await?;
if is_private == 0 && !password_protected {
level = std::cmp::max(level, AccessLevel::Write);
}
Ok(level)
}
fn require_write(level: AccessLevel) -> Result<(), ApiError> {
@@ -1159,18 +1246,12 @@ async fn has_header_resource_access(
kind: &str,
slug: &str,
) -> Result<bool, ApiError> {
for token in [
crate::security::resource_token(headers, kind, slug),
authorization_token(headers),
] {
if external_token_access_level(state, kind, slug, token).await? != AccessLevel::None {
return Ok(true);
}
}
Ok(account_token_access_level(
Ok(request_access_level(
state,
headers,
kind,
slug,
None,
crate::security::session_cookie_token(headers),
)
.await?
@@ -1241,7 +1322,7 @@ pub async fn authorized_workspace(
.await?
.ok_or_else(ApiError::not_found_workspace)?;
let token_level =
combined_token_access_level(state, "workspace", slug, access_token, bearer).await?;
request_access_level(state, headers, "workspace", slug, access_token, bearer).await?;
if workspace.is_private != 0 && token_level == AccessLevel::None {
return Err(ApiError::not_found_workspace());
}
@@ -1280,11 +1361,12 @@ async fn authorized_note(
Ok((workspace, note))
}
fn workspace_info_from(workspace: &db::Workspace) -> WorkspaceInfo {
fn workspace_info_from(workspace: &db::Workspace, access_level: AccessLevel) -> WorkspaceInfo {
WorkspaceInfo {
slug: workspace.slug.clone(),
title: workspace.title.clone(),
protected: workspace.password_hash.is_some(),
access_level: access_level_name(access_level).into(),
created_at: db::normalize_timestamp(&workspace.created_at),
updated_at: db::normalize_timestamp(&workspace.updated_at),
}