big update in share links

This commit is contained in:
Mateusz Gruszczyński
2026-08-03 09:56:47 +02:00
parent e3ee6319b9
commit fe5d00fcdd
27 changed files with 434 additions and 176 deletions
+16 -20
View File
@@ -96,25 +96,6 @@ pub async fn create_resource_access_token(
Ok(response)
}
pub async fn verify_resource_access_token(
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);
};
if crate::auth::share_access_permission(state, kind, slug, Some(token))
.await
.map_err(|error| ApiError::forbidden(&error.message))?
.is_some()
{
return Ok(true);
}
verify_password_access_token(state, kind, slug, Some(token)).await
}
pub(crate) async fn verify_password_access_token(
state: &SharedState,
kind: &str,
@@ -134,7 +115,22 @@ pub(crate) async fn verify_password_access_token(
.bind(Utc::now().to_rfc3339())
.fetch_one(state.db.pool())
.await?;
Ok(count > 0)
if count == 0 {
return Ok(false);
}
// Password-derived access must stop working when the resource no longer
// has a password. This also invalidates tokens created by older versions
// for private resources that never had a password configured.
match kind {
"workspace" => Ok(db::find_workspace(&state.db, slug)
.await?
.is_some_and(|workspace| workspace.password_hash.is_some())),
"pad" => Ok(db::find_pad(&state.db, slug)
.await?
.is_some_and(|pad| pad.password_hash.is_some())),
_ => Ok(false),
}
}
pub(super) fn hash_access_token(token: &str) -> String {
-1
View File
@@ -86,7 +86,6 @@ async fn share_session_redirect(
kind,
slug,
share,
crate::security::share_session_token(headers, kind, slug),
&client_key,
)
.await
+98 -35
View File
@@ -40,6 +40,7 @@ use crate::{
const MIN_PASSWORD: usize = 8;
const MAX_PASSWORD: usize = 128;
const MAX_NICKNAME: usize = 40;
const MAX_SHARE_LINK_LABEL: usize = 120;
#[derive(Debug, Clone)]
pub struct User {
@@ -149,6 +150,7 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for SharingUserRow {
#[derive(Debug)]
struct SharingLinkRow {
token_hash: String,
label: Option<String>,
permission: String,
expires_at: Option<String>,
created_at: String,
@@ -158,9 +160,10 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for SharingLinkRow {
fn from_row(row: &'r AnyRow) -> Result<Self, sqlx::Error> {
Ok(Self {
token_hash: crate::row_decode::text(row, 0)?,
permission: crate::row_decode::text(row, 1)?,
expires_at: crate::row_decode::optional_text(row, 2)?,
created_at: crate::row_decode::text(row, 3)?,
label: crate::row_decode::optional_text(row, 1)?,
permission: crate::row_decode::text(row, 2)?,
expires_at: crate::row_decode::optional_text(row, 3)?,
created_at: crate::row_decode::text(row, 4)?,
})
}
}
@@ -293,6 +296,8 @@ pub struct RemoveShareRequest {
pub struct CreateShareLinkRequest {
kind: String,
slug: String,
#[serde(default)]
label: Option<String>,
permission: String,
expires_at: Option<String>,
}
@@ -300,7 +305,9 @@ pub struct CreateShareLinkRequest {
pub struct UpdateShareLinkRequest {
kind: String,
slug: String,
token: String,
token_hash: String,
#[serde(default)]
label: Option<String>,
permission: String,
expires_at: Option<String>,
}
@@ -308,7 +315,7 @@ pub struct UpdateShareLinkRequest {
pub struct RevokeShareLinkRequest {
kind: String,
slug: String,
token: String,
token_hash: String,
}
#[derive(Serialize)]
@@ -361,8 +368,6 @@ pub struct IdentityResponse {
}
#[derive(Serialize)]
pub struct RegisterResponse {
#[serde(skip_serializing)]
token: Option<String>,
nickname: String,
email: String,
expires_at: Option<String>,
@@ -514,7 +519,6 @@ pub async fn register(
return Ok((
StatusCode::CREATED,
Json(RegisterResponse {
token: None,
nickname: user.nickname,
email: user.email,
expires_at: None,
@@ -534,7 +538,6 @@ pub async fn register(
let mut response = (
StatusCode::CREATED,
Json(RegisterResponse {
token: Some(session.token),
nickname: session.nickname,
email: session.email,
expires_at: Some(session.expires_at),
@@ -1441,6 +1444,40 @@ async fn ensure_owner(
Ok(())
}
pub(crate) async fn resource_is_public_unprotected(
state: &SharedState,
kind: &str,
slug: &str,
) -> Result<bool, AuthError> {
let slug = slug.trim();
match kind {
"workspace" => Ok(crate::db::find_workspace(&state.db, slug)
.await
.map_err(AuthError::database)?
.is_some_and(|workspace| {
workspace.is_private == 0 && workspace.password_hash.is_none()
})),
"pad" => Ok(crate::db::find_pad(&state.db, slug)
.await
.map_err(AuthError::database)?
.is_some_and(|pad| pad.is_private == 0 && pad.password_hash.is_none())),
_ => Ok(false),
}
}
async fn ensure_share_links_enabled(
state: &SharedState,
kind: &str,
slug: &str,
) -> Result<(), AuthError> {
if resource_is_public_unprotected(state, kind, slug).await? {
return Err(AuthError::conflict(
"Direct share links are disabled for public resources without a password.",
));
}
Ok(())
}
pub async fn set_resource_privacy(
State(state): State<SharedState>,
headers: HeaderMap,
@@ -1706,6 +1743,7 @@ pub async fn resource_sharing(
.get("slug")
.ok_or_else(|| AuthError::bad_request("Missing slug."))?;
ensure_owner(&state, owner.id, kind, slug).await?;
let share_links_enabled = !resource_is_public_unprotected(&state, kind, slug).await?;
let users: Vec<SharingUserRow> = sqlx::query_as(queries::get(
state.db.kind(),
queries::RESOURCE_SHARING_USERS,
@@ -1715,15 +1753,19 @@ pub async fn resource_sharing(
.fetch_all(state.db.pool())
.await
.map_err(AuthError::database)?;
let links: Vec<SharingLinkRow> = sqlx::query_as(queries::get(
state.db.kind(),
queries::RESOURCE_SHARING_LINKS,
))
.bind(kind)
.bind(slug)
.fetch_all(state.db.pool())
.await
.map_err(AuthError::database)?;
let links: Vec<SharingLinkRow> = if share_links_enabled {
sqlx::query_as(queries::get(
state.db.kind(),
queries::RESOURCE_SHARING_LINKS,
))
.bind(kind)
.bind(slug)
.fetch_all(state.db.pool())
.await
.map_err(AuthError::database)?
} else {
Vec::new()
};
let pending: Vec<PendingShareRow> = sqlx::query_as(queries::get(
state.db.kind(),
queries::RESOURCE_SHARING_PENDING,
@@ -1734,7 +1776,7 @@ pub async fn resource_sharing(
.await
.map_err(AuthError::database)?;
Ok(Json(
serde_json::json!({"users":users.into_iter().map(|row|serde_json::json!({"email":row.email,"nickname":row.nickname,"permission":row.permission})).collect::<Vec<_>>(), "pending":pending.into_iter().map(|row|serde_json::json!({"email":row.email,"nickname":row.nickname,"permission":row.permission,"expires_at":row.expires_at})).collect::<Vec<_>>(), "links":links.into_iter().map(|row|serde_json::json!({"token_hash":row.token_hash,"permission":row.permission,"expires_at":row.expires_at,"created_at":row.created_at})).collect::<Vec<_>>() }),
serde_json::json!({"users":users.into_iter().map(|row|serde_json::json!({"email":row.email,"nickname":row.nickname,"permission":row.permission})).collect::<Vec<_>>(), "pending":pending.into_iter().map(|row|serde_json::json!({"email":row.email,"nickname":row.nickname,"permission":row.permission,"expires_at":row.expires_at})).collect::<Vec<_>>(), "links":links.into_iter().map(|row|serde_json::json!({"token_hash":row.token_hash,"label":row.label,"permission":row.permission,"expires_at":row.expires_at,"created_at":row.created_at})).collect::<Vec<_>>(), "share_links_enabled":share_links_enabled }),
))
}
@@ -1745,12 +1787,15 @@ pub async fn create_share_link(
) -> Result<Response, AuthError> {
let owner = require_user(&state, &headers).await?;
ensure_owner(&state, owner.id, &req.kind, &req.slug).await?;
ensure_share_links_enabled(&state, &req.kind, &req.slug).await?;
let permission = validate_permission(&req.permission)?;
let label = normalize_share_link_label(req.label.as_deref())?;
let expires_at = normalize_share_expiration(req.expires_at.as_deref())?;
let token = random_token();
let token_hash = hash_token(&token);
sqlx::query(queries::get(state.db.kind(), queries::SHARE_LINK_INSERT))
.bind(token_hash)
.bind(&token_hash)
.bind(&label)
.bind(&req.kind)
.bind(req.slug.trim())
.bind(permission)
@@ -1765,7 +1810,7 @@ pub async fn create_share_link(
format!("/p/{}", req.slug.trim())
};
let mut response = Json(
serde_json::json!({"token":token,"url":format!("{base}?share={token}"),"permission":permission,"expires_at":expires_at}),
serde_json::json!({"token_hash":token_hash,"url":format!("{base}?share={token}"),"label":label,"permission":permission,"expires_at":expires_at}),
)
.into_response();
response.headers_mut().insert(
@@ -1786,12 +1831,15 @@ pub async fn update_share_link(
) -> Result<Json<serde_json::Value>, AuthError> {
let owner = require_user(&state, &headers).await?;
ensure_owner(&state, owner.id, &req.kind, &req.slug).await?;
ensure_share_links_enabled(&state, &req.kind, &req.slug).await?;
let permission = validate_permission(&req.permission)?;
let label = normalize_share_link_label(req.label.as_deref())?;
let expires_at = normalize_share_expiration(req.expires_at.as_deref())?;
let result = sqlx::query(queries::get(state.db.kind(), queries::SHARE_LINK_UPDATE))
.bind(&label)
.bind(permission)
.bind(&expires_at)
.bind(req.token.trim())
.bind(req.token_hash.trim())
.bind(&req.kind)
.bind(req.slug.trim())
.execute(state.db.pool())
@@ -1803,7 +1851,7 @@ pub async fn update_share_link(
));
}
Ok(Json(
serde_json::json!({"ok":true,"permission":permission,"expires_at":expires_at}),
serde_json::json!({"ok":true,"label":label,"permission":permission,"expires_at":expires_at}),
))
}
@@ -1814,9 +1862,10 @@ pub async fn revoke_share_link(
) -> Result<Json<serde_json::Value>, AuthError> {
let owner = require_user(&state, &headers).await?;
ensure_owner(&state, owner.id, &req.kind, &req.slug).await?;
ensure_share_links_enabled(&state, &req.kind, &req.slug).await?;
sqlx::query(queries::get(state.db.kind(), queries::SHARE_LINK_REVOKE))
.bind(Utc::now().to_rfc3339())
.bind(req.token.trim())
.bind(req.token_hash.trim())
.bind(&req.kind)
.bind(req.slug.trim())
.execute(state.db.pool())
@@ -1826,7 +1875,7 @@ pub async fn revoke_share_link(
state.db.kind(),
queries::SHARE_SESSIONS_DELETE_BY_LINK,
))
.bind(req.token.trim())
.bind(req.token_hash.trim())
.bind(&req.kind)
.bind(req.slug.trim())
.execute(state.db.pool())
@@ -1842,6 +1891,24 @@ fn validate_permission(value: &str) -> Result<&str, AuthError> {
}
}
fn normalize_share_link_label(value: Option<&str>) -> Result<Option<String>, AuthError> {
let Some(value) = value else {
return Ok(None);
};
let value = value.trim();
if value.is_empty() {
return Ok(None);
}
if value.chars().count() > MAX_SHARE_LINK_LABEL
|| value.chars().any(|character| character.is_control())
{
return Err(AuthError::bad_request(
"Link label must contain at most 120 printable characters.",
));
}
Ok(Some(value.to_owned()))
}
fn normalize_share_expiration(value: Option<&str>) -> Result<Option<String>, AuthError> {
let Some(value) = value else {
return Ok(None);
@@ -1903,9 +1970,11 @@ pub async fn create_share_session(
kind: &str,
slug: &str,
share_token: &str,
existing_session_token: Option<&str>,
client_key: &str,
) -> Result<Option<ShareSession>, AuthError> {
if resource_is_public_unprotected(state, kind, slug).await? {
return Ok(None);
}
let share_token = share_token.trim();
if !valid_share_token(share_token) || !matches!(kind, "workspace" | "pad") {
return Ok(None);
@@ -1939,15 +2008,6 @@ pub async fn create_share_session(
warn!(kind, slug, "invalid share link permission in database");
return Ok(None);
}
if source.permission == "ro"
&& share_session_permission(state, kind, slug, existing_session_token)
.await?
.as_deref()
== Some("rw")
{
return Ok(None);
}
let session_limit = now + Duration::days(state.anonymous_access_token_ttl_days);
let expires_at = match source.expires_at.as_deref() {
Some(value) => match chrono::DateTime::parse_from_rfc3339(value) {
@@ -2079,6 +2139,9 @@ pub async fn share_access_permission(
slug: &str,
token: Option<&str>,
) -> Result<Option<String>, AuthError> {
if resource_is_public_unprotected(state, kind, slug).await? {
return Ok(None);
}
let permission = share_session_permission(state, kind, slug, token).await?;
if permission.is_some() {
return Ok(permission);
+56 -2
View File
@@ -157,7 +157,7 @@ pub fn verify_workspace_password(workspace: &Workspace, password: Option<&str>)
&workspace.password_hash,
password.filter(|value| !value.is_empty()),
) {
(None, _) => true,
(None, _) => false,
(Some(hash), Some(password)) => PasswordHash::new(hash)
.ok()
.and_then(|parsed| {
@@ -433,7 +433,7 @@ pub fn verify_pad_password(pad: &Pad, password: Option<&str>) -> bool {
&pad.password_hash,
password.filter(|value| !value.is_empty()),
) {
(None, _) => true,
(None, _) => false,
(Some(hash), Some(password)) => PasswordHash::new(hash)
.ok()
.and_then(|parsed| {
@@ -544,3 +544,57 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for Pad {
})
}
}
#[cfg(test)]
mod password_verification_tests {
use super::*;
fn workspace(password_hash: Option<String>) -> Workspace {
Workspace {
id: 1,
slug: "private-workspace".into(),
title: "Private workspace".into(),
password_hash,
created_at: String::new(),
updated_at: String::new(),
is_private: 1,
}
}
fn pad(password_hash: Option<String>) -> Pad {
Pad {
id: 1,
slug: "private-pad".into(),
title: "Private pad".into(),
content: String::new(),
password_hash,
created_at: String::new(),
updated_at: String::new(),
owner_map: "[]".into(),
is_private: 1,
created_by_guest_id: None,
}
}
#[test]
fn missing_password_does_not_grant_password_access() {
assert!(!verify_workspace_password(&workspace(None), None));
assert!(!verify_workspace_password(&workspace(None), Some("anything")));
assert!(!verify_pad_password(&pad(None), None));
assert!(!verify_pad_password(&pad(None), Some("anything")));
}
#[test]
fn configured_password_is_verified() {
let workspace = workspace(Some(hash_password("workspace-secret")));
assert!(verify_workspace_password(
&workspace,
Some("workspace-secret")
));
assert!(!verify_workspace_password(&workspace, Some("wrong")));
let pad = pad(Some(hash_password("pad-secret")));
assert!(verify_pad_password(&pad, Some("pad-secret")));
assert!(!verify_pad_password(&pad, Some("wrong")));
}
}
+4 -1
View File
@@ -185,7 +185,7 @@ fn print_startup_credential() {
fn startup_credential() -> String {
format!(
"RustPad {}\nCopyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl",
"RustPad {}\nCopyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl\nLicense: https://git.linuxiarz.pl/gru/rustpad/src/branch/master/LICENSE.md",
env!("CARGO_PKG_VERSION")
)
}
@@ -304,6 +304,9 @@ mod startup_tests {
let credential = startup_credential();
assert!(credential.contains(&format!("RustPad {}", env!("CARGO_PKG_VERSION"))));
assert!(credential.contains("Mateusz Gruszczyński @linuxiarz.pl"));
assert!(credential.contains(
"https://git.linuxiarz.pl/gru/rustpad/src/branch/master/LICENSE.md"
));
}
}
+3 -3
View File
@@ -233,16 +233,16 @@ pub fn get(query: Query) -> &'static str {
r#"SELECT u.email, u.nickname, rp.permission FROM resource_permissions rp JOIN users u ON u.id = rp.user_id WHERE rp.resource_kind = ? AND rp.resource_slug = ? ORDER BY u.email"#
}
Query::RESOURCE_SHARING_LINKS => {
r#"SELECT token_hash, permission, CAST(expires_at AS CHAR CHARACTER SET utf8mb4) AS expires_at, CAST(created_at AS CHAR CHARACTER SET utf8mb4) AS created_at FROM resource_share_links WHERE resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL ORDER BY created_at DESC"#
r#"SELECT token_hash, CAST(label AS CHAR CHARACTER SET utf8mb4) AS label, permission, CAST(expires_at AS CHAR CHARACTER SET utf8mb4) AS expires_at, CAST(created_at AS CHAR CHARACTER SET utf8mb4) AS created_at FROM resource_share_links WHERE resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL ORDER BY created_at DESC"#
}
Query::RESOURCE_SHARING_PENDING => {
r#"SELECT u.email, u.nickname, i.permission, CAST(i.expires_at AS CHAR CHARACTER SET utf8mb4) AS expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = ? AND i.resource_slug = ? AND i.accepted_at IS NULL ORDER BY u.email"#
}
Query::SHARE_LINK_INSERT => {
r#"INSERT INTO resource_share_links (token_hash, resource_kind, resource_slug, permission, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?)"#
r#"INSERT INTO resource_share_links (token_hash, label, resource_kind, resource_slug, permission, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)"#
}
Query::SHARE_LINK_UPDATE => {
r#"UPDATE resource_share_links SET permission = ?, expires_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL"#
r#"UPDATE resource_share_links SET label = ?, permission = ?, expires_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL"#
}
Query::SHARE_LINK_REVOKE => {
r#"UPDATE resource_share_links SET revoked_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ?"#
+3 -3
View File
@@ -235,16 +235,16 @@ pub fn get(query: Query) -> &'static str {
r#"SELECT u.email, u.nickname, rp.permission FROM resource_permissions rp JOIN users u ON u.id = rp.user_id WHERE rp.resource_kind = $1 AND rp.resource_slug = $2 ORDER BY u.email"#
}
Query::RESOURCE_SHARING_LINKS => {
r#"SELECT token_hash, permission, expires_at, created_at FROM resource_share_links WHERE resource_kind = $1 AND resource_slug = $2 AND revoked_at IS NULL ORDER BY created_at DESC"#
r#"SELECT token_hash, label, permission, expires_at, created_at FROM resource_share_links WHERE resource_kind = $1 AND resource_slug = $2 AND revoked_at IS NULL ORDER BY created_at DESC"#
}
Query::RESOURCE_SHARING_PENDING => {
r#"SELECT u.email, u.nickname, i.permission, i.expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = $1 AND i.resource_slug = $2 AND i.accepted_at IS NULL ORDER BY u.email"#
}
Query::SHARE_LINK_INSERT => {
r#"INSERT INTO resource_share_links (token_hash, resource_kind, resource_slug, permission, expires_at, created_by) VALUES ($1, $2, $3, $4, $5, $6)"#
r#"INSERT INTO resource_share_links (token_hash, label, resource_kind, resource_slug, permission, expires_at, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7)"#
}
Query::SHARE_LINK_UPDATE => {
r#"UPDATE resource_share_links SET permission = $1, expires_at = $2 WHERE token_hash = $3 AND resource_kind = $4 AND resource_slug = $5 AND revoked_at IS NULL"#
r#"UPDATE resource_share_links SET label = $1, permission = $2, expires_at = $3 WHERE token_hash = $4 AND resource_kind = $5 AND resource_slug = $6 AND revoked_at IS NULL"#
}
Query::SHARE_LINK_REVOKE => {
r#"UPDATE resource_share_links SET revoked_at = $1 WHERE token_hash = $2 AND resource_kind = $3 AND resource_slug = $4"#
+3 -3
View File
@@ -233,16 +233,16 @@ pub fn get(query: Query) -> &'static str {
r#"SELECT u.email, u.nickname, rp.permission FROM resource_permissions rp JOIN users u ON u.id = rp.user_id WHERE rp.resource_kind = ? AND rp.resource_slug = ? ORDER BY u.email"#
}
Query::RESOURCE_SHARING_LINKS => {
r#"SELECT token_hash, permission, expires_at, created_at FROM resource_share_links WHERE resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL ORDER BY created_at DESC"#
r#"SELECT token_hash, label, permission, expires_at, created_at FROM resource_share_links WHERE resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL ORDER BY created_at DESC"#
}
Query::RESOURCE_SHARING_PENDING => {
r#"SELECT u.email, u.nickname, i.permission, i.expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = ? AND i.resource_slug = ? AND i.accepted_at IS NULL ORDER BY u.email"#
}
Query::SHARE_LINK_INSERT => {
r#"INSERT INTO resource_share_links (token_hash, resource_kind, resource_slug, permission, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?)"#
r#"INSERT INTO resource_share_links (token_hash, label, resource_kind, resource_slug, permission, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)"#
}
Query::SHARE_LINK_UPDATE => {
r#"UPDATE resource_share_links SET permission = ?, expires_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL"#
r#"UPDATE resource_share_links SET label = ?, permission = ?, expires_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL"#
}
Query::SHARE_LINK_REVOKE => {
r#"UPDATE resource_share_links SET revoked_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ?"#
-9
View File
@@ -126,10 +126,6 @@ pub fn share_session_cookie(
)
}
pub fn clear_share_session_cookie(kind: &str, slug: &str) -> HeaderValue {
clear_cookie(&share_session_cookie_name(kind, slug))
}
pub fn client_key(headers: &HeaderMap) -> String {
let forwarded_ip = header_ip(headers, "cf-connecting-ip")
.or_else(|| header_ip(headers, "x-real-ip"))
@@ -360,11 +356,6 @@ mod tests {
assert!(value.contains("HttpOnly"));
assert!(value.contains("Secure"));
assert!(value.contains("SameSite=Lax"));
let cleared = clear_share_session_cookie("workspace", "private-space")
.to_str()
.unwrap();
assert!(cleared.contains("Max-Age=0"));
}
#[test]
+8 -10
View File
@@ -249,15 +249,18 @@ async fn current_resource_access(
access_tokens: &[Option<&str>],
session_token: Option<&str>,
password_ok: bool,
public_unprotected: bool,
) -> (bool, bool) {
if auth::resource_is_public_unprotected(state, kind, slug)
.await
.unwrap_or(false)
{
return (true, true);
}
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 write_allowed =
password_ok || password_token_ok || permission.as_deref() == Some("rw");
let read_allowed = write_allowed || permission.as_deref() == Some("ro");
(read_allowed, write_allowed)
}
@@ -451,7 +454,6 @@ async fn handle_socket(
let _ = send_error(&mut socket, "Invalid password").await;
return;
}
let public_unprotected = workspace.is_private == 0 && workspace.password_hash.is_none();
let (_, write_allowed) = current_resource_access(
&state,
"workspace",
@@ -459,7 +461,6 @@ async fn handle_socket(
&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");
@@ -520,7 +521,6 @@ async fn handle_socket(
&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; }
@@ -549,7 +549,6 @@ async fn handle_socket(
&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;
@@ -564,7 +563,6 @@ async fn handle_socket(
&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;
-5
View File
@@ -218,7 +218,6 @@ async fn handle_pad_socket(
.await;
return;
}
let public_unprotected = pad.is_private == 0 && pad.password_hash.is_none();
let (_, write_allowed) = current_resource_access(
&state,
"pad",
@@ -226,7 +225,6 @@ async fn handle_pad_socket(
&external_tokens,
session_token.as_deref(),
password_ok,
public_unprotected,
)
.await;
info!(pad_id = pad.id, nickname = ?nickname, "pad websocket authenticated");
@@ -286,7 +284,6 @@ async fn handle_pad_socket(
&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;}
@@ -317,7 +314,6 @@ async fn handle_pad_socket(
&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;
@@ -332,7 +328,6 @@ async fn handle_pad_socket(
&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;