fix2 tokens

This commit is contained in:
Mateusz Gruszczyński committed 2026-08-03 01:35:05 +02:00
1 parent 49dad1a5f4
commit e3ee6319b9
25 files changed
+1420 -223

No files matched your search

+132 -41
View File
@@ -196,37 +196,70 @@ async fn resource_permission_from_tokens(
state: &SharedState,
kind: &str,
slug: &str,
access_token: Option<&str>,
access_tokens: &[Option<&str>],
session_token: Option<&str>,
) -> Option<String> {
let access_permission = auth::share_link_permission(state, kind, slug, access_token)
let mut read_allowed = false;
for token in access_tokens {
match auth::share_access_permission(state, kind, slug, *token)
.await
.ok()
.flatten()
.as_deref()
{
Some("rw") => return Some("rw".into()),
Some("ro") => read_allowed = true,
_ => {}
}
}
match auth::account_resource_permission(state, kind, slug, session_token)
.await
.ok()
.flatten();
let session_permission = auth::account_resource_permission(state, kind, slug, session_token)
.await
.ok()
.flatten();
if access_permission.as_deref() == Some("rw") || session_permission.as_deref() == Some("rw") {
Some("rw".into())
} else if access_permission.as_deref() == Some("ro")
|| session_permission.as_deref() == Some("ro")
.flatten()
.as_deref()
{
Some("ro".into())
} else {
None
Some("rw") => Some("rw".into()),
Some("ro") if !read_allowed => Some("ro".into()),
_ if read_allowed => Some("ro".into()),
_ => None,
}
}
async fn anonymous_access_from_tokens(
async fn password_access_from_tokens(
state: &SharedState,
kind: &str,
slug: &str,
access_token: Option<&str>,
access_tokens: &[Option<&str>],
) -> bool {
crate::api::verify_resource_access_token(state, kind, slug, access_token)
.await
.unwrap_or(false)
for token in access_tokens {
if crate::api::verify_password_access_token(state, kind, slug, *token)
.await
.unwrap_or(false)
{
return true;
}
}
false
}
async fn current_resource_access(
state: &SharedState,
kind: &str,
slug: &str,
access_tokens: &[Option<&str>],
session_token: Option<&str>,
password_ok: bool,
public_unprotected: bool,
) -> (bool, bool) {
let permission =
resource_permission_from_tokens(state, kind, slug, access_tokens, session_token).await;
let password_token_ok = password_access_from_tokens(state, kind, slug, access_tokens).await;
let write_allowed = public_unprotected
|| password_ok
|| password_token_ok
|| permission.as_deref() == Some("rw");
let read_allowed = write_allowed || permission.as_deref() == Some("ro");
(read_allowed, write_allowed)
}
// Merged from note.rs
@@ -241,6 +274,9 @@ pub async fn upgrade(
return (StatusCode::FORBIDDEN, "Invalid WebSocket origin").into_response();
}
let account_token = crate::security::session_token(&headers).map(str::to_owned);
let share_session_token =
crate::security::share_session_token(&headers, "workspace", &workspace_slug)
.map(str::to_owned);
let resource_token =
crate::security::resource_token(&headers, "workspace", &workspace_slug).map(str::to_owned);
let client_key = crate::security::client_key(&headers);
@@ -252,6 +288,7 @@ pub async fn upgrade(
workspace_slug,
note_slug,
account_token,
share_session_token,
resource_token,
client_key,
client_context,
@@ -265,7 +302,8 @@ async fn handle_socket(
workspace_slug: String,
note_slug: String,
cookie_session_token: Option<String>,
cookie_access_token: Option<String>,
cookie_share_session_token: Option<String>,
cookie_password_token: Option<String>,
client_key: String,
client_context: RequestClientContext,
) {
@@ -319,7 +357,11 @@ async fn handle_socket(
.map(str::trim)
.filter(|value| !value.is_empty() && *value != "cookie")
.map(str::to_owned);
let access_token = explicit_access_token.or(cookie_access_token);
let external_tokens = [
explicit_access_token.as_deref(),
cookie_share_session_token.as_deref(),
cookie_password_token.as_deref(),
];
let nickname = match auth::authorize_nickname(&state, nickname, session_token.clone()).await {
Ok(value) => value,
Err(message) => {
@@ -343,18 +385,17 @@ async fn handle_socket(
&state,
"workspace",
&workspace_slug,
access_token.as_deref(),
&external_tokens,
session_token.as_deref(),
)
.await;
let anonymous_token_ok = permission.is_none()
&& anonymous_access_from_tokens(
&state,
"workspace",
&workspace_slug,
access_token.as_deref(),
)
.await;
let anonymous_token_ok = password_access_from_tokens(
&state,
"workspace",
&workspace_slug,
&external_tokens,
)
.await;
let password_limit_key = format!("resource-password:{client_key}:workspace:{workspace_slug}");
let password_attempted = password
.as_deref()
@@ -410,10 +451,17 @@ async fn handle_socket(
let _ = send_error(&mut socket, "Invalid password").await;
return;
}
let write_allowed = permission.as_deref() == Some("rw")
|| anonymous_token_ok
|| password_ok
|| (workspace.is_private == 0 && workspace.password_hash.is_none() && permission.is_none());
let public_unprotected = workspace.is_private == 0 && workspace.password_hash.is_none();
let (_, write_allowed) = current_resource_access(
&state,
"workspace",
&workspace_slug,
&external_tokens,
session_token.as_deref(),
password_ok,
public_unprotected,
)
.await;
info!(workspace_id = workspace.id, note_id = note.id, nickname = ?nickname, "note websocket authenticated");
if send(
&mut socket,
@@ -443,6 +491,9 @@ async fn handle_socket(
.await;
let _ = channel.send(RoomEvent::Presence(users));
let mut last_chat = Instant::now() - Duration::from_secs(1);
let mut access_refresh = tokio::time::interval(Duration::from_secs(10));
access_refresh.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
access_refresh.tick().await;
let (mut sender, mut receiver) = socket.split();
if send_split(
&mut sender,
@@ -462,7 +513,17 @@ async fn handle_socket(
incoming=receiver.next()=>match incoming {
Some(Ok(Message::Text(text)))=>match serde_json::from_str::<ClientMessage>(&text) {
Ok(ClientMessage::Update{content,owner_map})=>{
if !write_allowed { let _=send_split(&mut sender,&ServerMessage::Error{message:"Read-only access".into()}).await; continue; }
let (read_allowed, current_write_allowed) = current_resource_access(
&state,
"workspace",
&workspace_slug,
&external_tokens,
session_token.as_deref(),
password_ok,
public_unprotected,
).await;
if !read_allowed { let _=send_split(&mut sender,&ServerMessage::Error{message:"Access expired or revoked".into()}).await; break; }
if !current_write_allowed { let _=send_split(&mut sender,&ServerMessage::Error{message:"Read-only access".into()}).await; continue; }
if content.len()>2_000_000 { let _=send_split(&mut sender,&ServerMessage::Error{message:"The document is too large".into()}).await; continue; }
let owner_map=owner_map.unwrap_or_else(||"[]".into());
match db::save_revision(&state.db,note.id,workspace.id,&content,nickname.as_deref(),&owner_map).await {
@@ -480,12 +541,42 @@ async fn handle_socket(
},
Some(Ok(Message::Close(_)))|None=>break, Some(Ok(_))=>{}, Some(Err(error))=>{debug!(%error,"websocket receive error");break;}
},
update=updates.recv()=>match update {
Ok(RoomEvent::Document(update))=>if send_split(&mut sender,&ServerMessage::Document{content:update.content,revision_id:update.revision_id,updated_at:update.updated_at,author:update.author,owner_map:update.owner_map}).await.is_err(){break;},
Ok(RoomEvent::Presence(users))=>if send_split(&mut sender,&ServerMessage::Presence{users}).await.is_err(){break;},
Ok(RoomEvent::Chat{sender:chat_sender,text})=>if send_split(&mut sender,&ServerMessage::Chat{sender:chat_sender,text}).await.is_err(){break;},
Err(tokio::sync::broadcast::error::RecvError::Lagged(_))=>if let Ok(Some(current))=db::find_note(&state.db,workspace.id,&note_slug).await { if send_split(&mut sender,&ServerMessage::Document{content:current.content,revision_id:0,updated_at:current.updated_at,author:None,owner_map:current.owner_map}).await.is_err(){break;} },
Err(tokio::sync::broadcast::error::RecvError::Closed)=>break,
_=access_refresh.tick()=>{
let (read_allowed, _) = current_resource_access(
&state,
"workspace",
&workspace_slug,
&external_tokens,
session_token.as_deref(),
password_ok,
public_unprotected,
).await;
if !read_allowed {
let _=send_split(&mut sender,&ServerMessage::Error{message:"Access expired or revoked".into()}).await;
break;
}
},
update=updates.recv()=>{
let (read_allowed, _) = current_resource_access(
&state,
"workspace",
&workspace_slug,
&external_tokens,
session_token.as_deref(),
password_ok,
public_unprotected,
).await;
if !read_allowed {
let _=send_split(&mut sender,&ServerMessage::Error{message:"Access expired or revoked".into()}).await;
break;
}
match update {
Ok(RoomEvent::Document(update))=>if send_split(&mut sender,&ServerMessage::Document{content:update.content,revision_id:update.revision_id,updated_at:update.updated_at,author:update.author,owner_map:update.owner_map}).await.is_err(){break;},
Ok(RoomEvent::Presence(users))=>if send_split(&mut sender,&ServerMessage::Presence{users}).await.is_err(){break;},
Ok(RoomEvent::Chat{sender:chat_sender,text})=>if send_split(&mut sender,&ServerMessage::Chat{sender:chat_sender,text}).await.is_err(){break;},
Err(tokio::sync::broadcast::error::RecvError::Lagged(_))=>if let Ok(Some(current))=db::find_note(&state.db,workspace.id,&note_slug).await { if send_split(&mut sender,&ServerMessage::Document{content:current.content,revision_id:0,updated_at:current.updated_at,author:None,owner_map:current.owner_map}).await.is_err(){break;} },
Err(tokio::sync::broadcast::error::RecvError::Closed)=>break,
}
}
}
}
+75 -16
View File
@@ -44,6 +44,8 @@ pub async fn upgrade_pad(
return (StatusCode::FORBIDDEN, "Invalid WebSocket origin").into_response();
}
let account_token = crate::security::session_token(&headers).map(str::to_owned);
let share_session_token =
crate::security::share_session_token(&headers, "pad", &slug).map(str::to_owned);
let resource_token = crate::security::resource_token(&headers, "pad", &slug).map(str::to_owned);
let client_key = crate::security::client_key(&headers);
let client_context = RequestClientContext::from_headers(&headers, &client_key);
@@ -53,6 +55,7 @@ pub async fn upgrade_pad(
state,
slug,
account_token,
share_session_token,
resource_token,
client_key,
client_context,
@@ -64,7 +67,8 @@ async fn handle_pad_socket(
state: SharedState,
slug: String,
cookie_session_token: Option<String>,
cookie_access_token: Option<String>,
cookie_share_session_token: Option<String>,
cookie_password_token: Option<String>,
client_key: String,
client_context: RequestClientContext,
) {
@@ -117,7 +121,11 @@ async fn handle_pad_socket(
.map(str::trim)
.filter(|value| !value.is_empty() && *value != "cookie")
.map(str::to_owned);
let access_token = explicit_access_token.or(cookie_access_token);
let external_tokens = [
explicit_access_token.as_deref(),
cookie_share_session_token.as_deref(),
cookie_password_token.as_deref(),
];
let nickname = match auth::authorize_nickname(&state, nickname, session_token.clone()).await {
Ok(value) => value,
Err(message) => {
@@ -141,12 +149,12 @@ async fn handle_pad_socket(
&state,
"pad",
&slug,
access_token.as_deref(),
&external_tokens,
session_token.as_deref(),
)
.await;
let anonymous_token_ok = permission.is_none()
&& anonymous_access_from_tokens(&state, "pad", &slug, access_token.as_deref()).await;
let anonymous_token_ok =
password_access_from_tokens(&state, "pad", &slug, &external_tokens).await;
let password_limit_key = format!("resource-password:{client_key}:pad:{slug}");
let password_attempted = password
.as_deref()
@@ -210,10 +218,17 @@ async fn handle_pad_socket(
.await;
return;
}
let write_allowed = permission.as_deref() == Some("rw")
|| anonymous_token_ok
|| password_ok
|| (pad.is_private == 0 && pad.password_hash.is_none() && permission.is_none());
let public_unprotected = pad.is_private == 0 && pad.password_hash.is_none();
let (_, write_allowed) = current_resource_access(
&state,
"pad",
&slug,
&external_tokens,
session_token.as_deref(),
password_ok,
public_unprotected,
)
.await;
info!(pad_id = pad.id, nickname = ?nickname, "pad websocket authenticated");
if send_pad(
&mut socket,
@@ -242,6 +257,9 @@ async fn handle_pad_socket(
.await;
let _ = channel.send(RoomEvent::Presence(users));
let mut last_chat = Instant::now() - Duration::from_secs(1);
let mut access_refresh = tokio::time::interval(Duration::from_secs(10));
access_refresh.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
access_refresh.tick().await;
let (mut sender, mut receiver) = socket.split();
if send_pad_split(
&mut sender,
@@ -260,7 +278,18 @@ async fn handle_pad_socket(
tokio::select! {
incoming=receiver.next()=>match incoming{
Some(Ok(Message::Text(text)))=>match serde_json::from_str::<ClientMessage>(&text){
Ok(ClientMessage::Update{content,owner_map})=>{if !write_allowed{let _=send_pad_split(&mut sender,&PadServerMessage::Error{message:"Read-only access".into()}).await;continue;}
Ok(ClientMessage::Update{content,owner_map})=>{
let (read_allowed, current_write_allowed) = current_resource_access(
&state,
"pad",
&slug,
&external_tokens,
session_token.as_deref(),
password_ok,
public_unprotected,
).await;
if !read_allowed { let _=send_pad_split(&mut sender,&PadServerMessage::Error{message:"Access expired or revoked".into()}).await;break; }
if !current_write_allowed{let _=send_pad_split(&mut sender,&PadServerMessage::Error{message:"Read-only access".into()}).await;continue;}
if content.len()>2_000_000 { let _=send_pad_split(&mut sender,&PadServerMessage::Error{message:"The document is too large".into()}).await; continue; }
let owner_map=owner_map.unwrap_or_else(||"[]".into());
if let Ok((revision_id,updated_at))=db::save_pad_revision(&state.db,pad.id,&content,nickname.as_deref(),&owner_map).await{
@@ -280,12 +309,42 @@ async fn handle_pad_socket(
Some(Ok(_))=>{},
Some(Err(error))=>{debug!(%error,"pad websocket receive error");break;}
},
update=updates.recv()=>match update{
Ok(RoomEvent::Document(u))=>if send_pad_split(&mut sender,&PadServerMessage::Document{content:u.content,revision_id:u.revision_id,updated_at:u.updated_at,author:u.author,owner_map:u.owner_map}).await.is_err(){break;},
Ok(RoomEvent::Presence(users))=>if send_pad_split(&mut sender,&PadServerMessage::Presence{users}).await.is_err(){break;},
Ok(RoomEvent::Chat{sender:chat_sender,text})=>if send_pad_split(&mut sender,&PadServerMessage::Chat{sender:chat_sender,text}).await.is_err(){break;},
Err(tokio::sync::broadcast::error::RecvError::Lagged(_))=>if let Ok(Some(current))=db::find_pad(&state.db,&slug).await { if send_pad_split(&mut sender,&PadServerMessage::Document{content:current.content,revision_id:0,updated_at:current.updated_at,author:None,owner_map:current.owner_map}).await.is_err(){break;} },
Err(tokio::sync::broadcast::error::RecvError::Closed)=>break,
_=access_refresh.tick()=>{
let (read_allowed, _) = current_resource_access(
&state,
"pad",
&slug,
&external_tokens,
session_token.as_deref(),
password_ok,
public_unprotected,
).await;
if !read_allowed {
let _=send_pad_split(&mut sender,&PadServerMessage::Error{message:"Access expired or revoked".into()}).await;
break;
}
},
update=updates.recv()=>{
let (read_allowed, _) = current_resource_access(
&state,
"pad",
&slug,
&external_tokens,
session_token.as_deref(),
password_ok,
public_unprotected,
).await;
if !read_allowed {
let _=send_pad_split(&mut sender,&PadServerMessage::Error{message:"Access expired or revoked".into()}).await;
break;
}
match update {
Ok(RoomEvent::Document(u))=>if send_pad_split(&mut sender,&PadServerMessage::Document{content:u.content,revision_id:u.revision_id,updated_at:u.updated_at,author:u.author,owner_map:u.owner_map}).await.is_err(){break;},
Ok(RoomEvent::Presence(users))=>if send_pad_split(&mut sender,&PadServerMessage::Presence{users}).await.is_err(){break;},
Ok(RoomEvent::Chat{sender:chat_sender,text})=>if send_pad_split(&mut sender,&PadServerMessage::Chat{sender:chat_sender,text}).await.is_err(){break;},
Err(tokio::sync::broadcast::error::RecvError::Lagged(_))=>if let Ok(Some(current))=db::find_pad(&state.db,&slug).await { if send_pad_split(&mut sender,&PadServerMessage::Document{content:current.content,revision_id:0,updated_at:current.updated_at,author:None,owner_map:current.owner_map}).await.is_err(){break;} },
Err(tokio::sync::broadcast::error::RecvError::Closed)=>break,
}
}
}
}