security upgrade
This commit is contained in:
@@ -18,16 +18,33 @@ pub struct AccessTokenRequest {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AccessTokenResponse {
|
||||
access_token: String,
|
||||
granted: bool,
|
||||
expires_at: String,
|
||||
}
|
||||
|
||||
pub async fn create_resource_access_token(
|
||||
State(state): State<SharedState>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<AccessTokenRequest>,
|
||||
) -> Result<Json<AccessTokenResponse>, ApiError> {
|
||||
) -> Result<Response, ApiError> {
|
||||
let kind = payload.kind.trim();
|
||||
let slug = payload.slug.trim();
|
||||
let client_key = crate::security::client_key(&headers);
|
||||
let client_limit_key = format!("resource-password-client:{client_key}");
|
||||
let limit_key = format!("resource-password:{client_key}:{kind}:{slug}");
|
||||
let window = std::time::Duration::from_secs(15 * 60);
|
||||
state
|
||||
.check_rate_limit(client_limit_key, 50, window)
|
||||
.await
|
||||
.map_err(|seconds| ApiError::rate_limited(&format!(
|
||||
"Too many password attempts. Try again in {seconds} seconds."
|
||||
)))?;
|
||||
state
|
||||
.check_rate_limit(limit_key.clone(), 10, window)
|
||||
.await
|
||||
.map_err(|seconds| ApiError::rate_limited(&format!(
|
||||
"Too many password attempts. Try again in {seconds} seconds."
|
||||
)))?;
|
||||
match kind {
|
||||
"workspace" => {
|
||||
let workspace = db::find_workspace(&state.db, slug)
|
||||
@@ -63,10 +80,20 @@ pub async fn create_resource_access_token(
|
||||
.bind(&expires_at)
|
||||
.execute(state.db.pool())
|
||||
.await?;
|
||||
Ok(Json(AccessTokenResponse {
|
||||
access_token: token,
|
||||
state.clear_rate_limit(&limit_key).await;
|
||||
let cookie = crate::security::resource_cookie(
|
||||
kind,
|
||||
slug,
|
||||
&token,
|
||||
state.anonymous_access_token_ttl_days,
|
||||
);
|
||||
let mut response = Json(AccessTokenResponse {
|
||||
granted: true,
|
||||
expires_at,
|
||||
}))
|
||||
})
|
||||
.into_response();
|
||||
response.headers_mut().insert(header::SET_COOKIE, cookie);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn verify_resource_access_token(
|
||||
@@ -78,7 +105,7 @@ pub async fn verify_resource_access_token(
|
||||
let Some(token) = token.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return Ok(false);
|
||||
};
|
||||
if crate::auth::resource_permission(state, kind, slug, Some(token))
|
||||
if crate::auth::share_link_permission(state, kind, slug, Some(token))
|
||||
.await
|
||||
.map_err(|error| ApiError::forbidden(&error.message))?
|
||||
.is_some()
|
||||
|
||||
@@ -25,6 +25,12 @@ impl ApiError {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
pub(crate) fn rate_limited(message: &str) -> Self {
|
||||
Self {
|
||||
status: StatusCode::TOO_MANY_REQUESTS,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
pub(crate) fn payload_too_large(max_bytes: usize) -> Self {
|
||||
let max_mb = max_bytes / (1024 * 1024);
|
||||
Self {
|
||||
|
||||
+77
-12
@@ -16,6 +16,7 @@ pub async fn upload_pad_file(
|
||||
Path(slug): Path<String>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
require_authenticated_upload(&state, &headers).await?;
|
||||
let mut password: Option<String> = None;
|
||||
let mut access_token: Option<String> = None;
|
||||
let mut file: Option<(String, Vec<u8>)> = None;
|
||||
@@ -55,8 +56,9 @@ pub async fn upload_pad_file(
|
||||
&state,
|
||||
&slug,
|
||||
password.as_deref(),
|
||||
access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let level = if db::verify_pad_password(&pad, password.as_deref())
|
||||
@@ -68,7 +70,7 @@ pub async fn upload_pad_file(
|
||||
&state,
|
||||
"pad",
|
||||
&slug,
|
||||
access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -144,8 +146,9 @@ pub async fn pad_files(
|
||||
&state,
|
||||
&slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let mut files = db::list_pad_files(&state.db, pad.id).await?;
|
||||
@@ -175,8 +178,9 @@ pub async fn delete_pad_file(
|
||||
&state,
|
||||
&slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
if !crate::auth::is_resource_owner(&state, "pad", &pad.slug, bearer_token(&headers))
|
||||
@@ -201,6 +205,7 @@ pub async fn upload_note_file(
|
||||
Path((workspace_slug, note_slug)): Path<(String, String)>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
require_authenticated_upload(&state, &headers).await?;
|
||||
let mut password: Option<String> = None;
|
||||
let mut access_token: Option<String> = None;
|
||||
let mut file: Option<(String, Vec<u8>)> = None;
|
||||
@@ -241,8 +246,9 @@ pub async fn upload_note_file(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
password.as_deref(),
|
||||
access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -255,7 +261,7 @@ pub async fn upload_note_file(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -309,8 +315,9 @@ pub async fn delete_note(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -323,7 +330,7 @@ pub async fn delete_note(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -360,8 +367,9 @@ pub async fn note_files(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let mut files = db::list_note_files(&state.db, note.id).await?;
|
||||
@@ -392,8 +400,9 @@ pub async fn delete_note_file(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let workspace_owner = crate::auth::is_resource_owner(
|
||||
@@ -429,6 +438,19 @@ pub async fn delete_note_file(
|
||||
Ok(Json(serde_json::json!({"ok": true})))
|
||||
}
|
||||
|
||||
async fn require_authenticated_upload(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
) -> Result<(), ApiError> {
|
||||
let user = crate::auth::optional_user(state, headers)
|
||||
.await
|
||||
.map_err(|error| ApiError::forbidden(&error.message))?;
|
||||
if user.is_none() {
|
||||
return Err(ApiError::forbidden("Log in to upload files."));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_file(
|
||||
State(state): State<SharedState>,
|
||||
Path((token, filename)): Path<(String, String)>,
|
||||
@@ -476,13 +498,29 @@ async fn serve_token_file(
|
||||
.get_local_with_legacy(&key, &legacy_key)
|
||||
.await
|
||||
.map_err(|_| ApiError::not_found_file())?;
|
||||
let mime = mime_guess::from_path(&safe).first_or_octet_stream();
|
||||
let guessed_mime = mime_guess::from_path(&safe).first_or_octet_stream();
|
||||
let inline_image = is_safe_inline_image_mime(guessed_mime.essence_str());
|
||||
let served_mime = if inline_image {
|
||||
guessed_mime.as_ref()
|
||||
} else {
|
||||
"application/octet-stream"
|
||||
};
|
||||
let disposition = if inline_image { "inline" } else { "attachment" };
|
||||
let mut response = bytes.into_response();
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_str(mime.as_ref())
|
||||
HeaderValue::from_str(served_mime)
|
||||
.unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_DISPOSITION,
|
||||
HeaderValue::from_str(&format!("{disposition}; filename=\"{safe}\""))
|
||||
.expect("sanitized attachment filename"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static("content-security-policy"),
|
||||
HeaderValue::from_static("default-src 'none'; sandbox"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::X_CONTENT_TYPE_OPTIONS,
|
||||
HeaderValue::from_static("nosniff"),
|
||||
@@ -501,6 +539,19 @@ async fn serve_token_file(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn is_safe_inline_image_mime(value: &str) -> bool {
|
||||
matches!(
|
||||
value,
|
||||
"image/png"
|
||||
| "image/jpeg"
|
||||
| "image/gif"
|
||||
| "image/webp"
|
||||
| "image/avif"
|
||||
| "image/bmp"
|
||||
| "image/x-icon"
|
||||
)
|
||||
}
|
||||
|
||||
fn sanitize_filename(value: &str) -> String {
|
||||
let name = std::path::Path::new(value)
|
||||
.file_name()
|
||||
@@ -522,3 +573,17 @@ fn sanitize_filename(value: &str) -> String {
|
||||
clean.chars().take(160).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::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"));
|
||||
}
|
||||
}
|
||||
|
||||
+138
-39
@@ -40,21 +40,28 @@ const MAX_PASSWORD_LENGTH: usize = 128;
|
||||
const MIN_WORKSPACE_SLUG_LENGTH: usize = 6;
|
||||
|
||||
fn bearer_token(headers: &HeaderMap) -> Option<&str> {
|
||||
headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.strip_prefix("Bearer "))
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
crate::security::session_token(headers)
|
||||
}
|
||||
|
||||
fn authorization_token(headers: &HeaderMap) -> Option<&str> {
|
||||
crate::security::bearer_token(headers)
|
||||
}
|
||||
|
||||
fn user_session_token(headers: &HeaderMap) -> Option<&str> {
|
||||
headers
|
||||
.get("x-rustpad-user-token")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
crate::security::session_token(headers)
|
||||
}
|
||||
|
||||
fn resource_request_token<'a>(
|
||||
headers: &'a HeaderMap,
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
supplied: Option<&'a str>,
|
||||
) -> Option<&'a str> {
|
||||
supplied
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.or_else(|| bearer_token(headers))
|
||||
.filter(|value| !value.is_empty() && *value != "cookie")
|
||||
.or_else(|| crate::security::resource_token(headers, kind, slug))
|
||||
.or_else(|| authorization_token(headers))
|
||||
}
|
||||
|
||||
async fn session_user(
|
||||
@@ -75,14 +82,19 @@ async fn has_write_permission(
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
) -> Result<bool, ApiError> {
|
||||
let bearer = bearer_token(headers);
|
||||
if token_access_level(state, kind, slug, bearer).await? >= AccessLevel::Write {
|
||||
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 session = user_session_token(headers);
|
||||
if session.is_some() && session != bearer {
|
||||
return Ok(token_access_level(state, kind, slug, session).await? >= AccessLevel::Write);
|
||||
let authorization = authorization_token(headers);
|
||||
if authorization != resource
|
||||
&& external_token_access_level(state, kind, slug, authorization).await? >= AccessLevel::Write
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
let session = crate::security::session_cookie_token(headers);
|
||||
if session != resource && session != authorization {
|
||||
return Ok(account_token_access_level(state, kind, slug, session).await? >= AccessLevel::Write);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
@@ -476,10 +488,10 @@ pub async fn workspace_info(
|
||||
.ok_or_else(ApiError::not_found_workspace)?;
|
||||
ensure_private_resource_access(
|
||||
&state,
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace.slug,
|
||||
workspace.is_private,
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(workspace_info_from(&workspace)))
|
||||
@@ -495,8 +507,9 @@ pub async fn open_workspace(
|
||||
&state,
|
||||
&workspace_slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let stats = db::list_note_stats(&state.db, workspace.id)
|
||||
@@ -541,8 +554,9 @@ pub async fn create_note(
|
||||
&state,
|
||||
&workspace_slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let level = if db::verify_workspace_password(&workspace, payload.password.as_deref())
|
||||
@@ -554,7 +568,7 @@ pub async fn create_note(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -692,10 +706,10 @@ pub async fn note_info(
|
||||
.ok_or_else(ApiError::not_found_workspace)?;
|
||||
ensure_private_resource_access(
|
||||
&state,
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace.slug,
|
||||
workspace.is_private,
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?;
|
||||
let note = db::find_note(&state.db, workspace.id, ¬e_slug)
|
||||
@@ -807,8 +821,9 @@ pub async fn history(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let _ = workspace;
|
||||
@@ -834,8 +849,9 @@ pub async fn restore(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let level = if db::verify_workspace_password(&workspace, payload.password.as_deref())
|
||||
@@ -847,7 +863,7 @@ pub async fn restore(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -919,13 +935,13 @@ async fn anonymous_access_token_valid(
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
async fn token_access_level(
|
||||
async fn external_token_access_level(
|
||||
state: &SharedState,
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
token: Option<&str>,
|
||||
) -> Result<AccessLevel, ApiError> {
|
||||
let permission = crate::auth::resource_permission(state, kind, slug, token)
|
||||
let permission = crate::auth::share_link_permission(state, kind, slug, token)
|
||||
.await
|
||||
.map_err(|error| ApiError::forbidden(&error.message))?;
|
||||
let level = permission_level(permission.as_deref());
|
||||
@@ -940,16 +956,28 @@ async fn token_access_level(
|
||||
Ok(AccessLevel::None)
|
||||
}
|
||||
|
||||
async fn account_token_access_level(
|
||||
state: &SharedState,
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
token: Option<&str>,
|
||||
) -> Result<AccessLevel, ApiError> {
|
||||
let permission = crate::auth::account_resource_permission(state, kind, slug, token)
|
||||
.await
|
||||
.map_err(|error| ApiError::forbidden(&error.message))?;
|
||||
Ok(permission_level(permission.as_deref()))
|
||||
}
|
||||
|
||||
async fn combined_token_access_level(
|
||||
state: &SharedState,
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
access_token: Option<&str>,
|
||||
bearer: Option<&str>,
|
||||
account_token: Option<&str>,
|
||||
) -> Result<AccessLevel, ApiError> {
|
||||
Ok(std::cmp::max(
|
||||
token_access_level(state, kind, slug, access_token).await?,
|
||||
token_access_level(state, kind, slug, bearer).await?,
|
||||
external_token_access_level(state, kind, slug, access_token).await?,
|
||||
account_token_access_level(state, kind, slug, account_token).await?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -961,28 +989,89 @@ fn require_write(level: AccessLevel) -> Result<(), ApiError> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn has_header_resource_access(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
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(
|
||||
state,
|
||||
kind,
|
||||
slug,
|
||||
crate::security::session_cookie_token(headers),
|
||||
)
|
||||
.await?
|
||||
!= AccessLevel::None)
|
||||
}
|
||||
|
||||
async fn ensure_private_resource_access(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
is_private: i64,
|
||||
token: Option<&str>,
|
||||
) -> Result<(), ApiError> {
|
||||
if is_private == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
if verify_resource_access_token(state, kind, slug, token).await? {
|
||||
if has_header_resource_access(state, headers, kind, slug).await? {
|
||||
return Ok(());
|
||||
}
|
||||
Err(ApiError::not_found_workspace())
|
||||
}
|
||||
|
||||
async fn check_resource_password_attempt(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
password: Option<&str>,
|
||||
password_ok: bool,
|
||||
) -> Result<(), ApiError> {
|
||||
let Some(_) = password.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return Ok(());
|
||||
};
|
||||
let client_key = crate::security::client_key(headers);
|
||||
let window = std::time::Duration::from_secs(15 * 60);
|
||||
state
|
||||
.check_rate_limit(format!("resource-password-client:{client_key}"), 50, window)
|
||||
.await
|
||||
.map_err(|seconds| {
|
||||
ApiError::rate_limited(&format!(
|
||||
"Too many password attempts. Try again in {seconds} seconds."
|
||||
))
|
||||
})?;
|
||||
let limit_key = format!("resource-password:{client_key}:{kind}:{slug}");
|
||||
state
|
||||
.check_rate_limit(limit_key.clone(), 10, window)
|
||||
.await
|
||||
.map_err(|seconds| {
|
||||
ApiError::rate_limited(&format!(
|
||||
"Too many password attempts. Try again in {seconds} seconds."
|
||||
))
|
||||
})?;
|
||||
if password_ok {
|
||||
state.clear_rate_limit(&limit_key).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn authorized_workspace(
|
||||
state: &SharedState,
|
||||
slug: &str,
|
||||
password: Option<&str>,
|
||||
access_token: Option<&str>,
|
||||
bearer: Option<&str>,
|
||||
headers: &HeaderMap,
|
||||
) -> Result<db::Workspace, ApiError> {
|
||||
let workspace = db::find_workspace(&state.db, slug)
|
||||
.await?
|
||||
@@ -992,11 +1081,13 @@ pub async fn authorized_workspace(
|
||||
if workspace.is_private != 0 && token_level == AccessLevel::None {
|
||||
return Err(ApiError::not_found_workspace());
|
||||
}
|
||||
if workspace.password_hash.is_some()
|
||||
&& !db::verify_workspace_password(&workspace, password)
|
||||
&& token_level == AccessLevel::None
|
||||
{
|
||||
return Err(ApiError::unauthorized());
|
||||
if workspace.password_hash.is_some() && token_level < AccessLevel::Write {
|
||||
let password_ok = db::verify_workspace_password(&workspace, password);
|
||||
check_resource_password_attempt(state, headers, "workspace", slug, password, password_ok)
|
||||
.await?;
|
||||
if token_level == AccessLevel::None && !password_ok {
|
||||
return Err(ApiError::unauthorized());
|
||||
}
|
||||
}
|
||||
Ok(workspace)
|
||||
}
|
||||
@@ -1008,9 +1099,17 @@ async fn authorized_note(
|
||||
password: Option<&str>,
|
||||
access_token: Option<&str>,
|
||||
bearer: Option<&str>,
|
||||
headers: &HeaderMap,
|
||||
) -> Result<(db::Workspace, db::Note), ApiError> {
|
||||
let workspace =
|
||||
authorized_workspace(state, workspace_slug, password, access_token, bearer).await?;
|
||||
let workspace = authorized_workspace(
|
||||
state,
|
||||
workspace_slug,
|
||||
password,
|
||||
access_token,
|
||||
bearer,
|
||||
headers,
|
||||
)
|
||||
.await?;
|
||||
let note = db::find_note(&state.db, workspace.id, note_slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
|
||||
+47
-18
@@ -94,10 +94,10 @@ pub async fn pad_info(
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
ensure_private_resource_access(
|
||||
&state,
|
||||
&headers,
|
||||
"pad",
|
||||
&pad.slug,
|
||||
pad.is_private,
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?;
|
||||
let (global_color, note_color) = editor_colors(&state, &headers, "pad", &slug).await?;
|
||||
@@ -238,8 +238,9 @@ pub async fn publish_pad_page(
|
||||
&state,
|
||||
&slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let level = if db::verify_pad_password(&pad, payload.password.as_deref())
|
||||
@@ -251,7 +252,7 @@ pub async fn publish_pad_page(
|
||||
&state,
|
||||
"pad",
|
||||
&slug,
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -287,8 +288,14 @@ pub async fn publish_note_page(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let level = if db::verify_workspace_password(&workspace, payload.password.as_deref())
|
||||
@@ -300,7 +307,12 @@ pub async fn publish_note_page(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -339,7 +351,6 @@ async fn ensure_public_page_access(
|
||||
page: &db::PublishedPage,
|
||||
) -> Result<(), ApiError> {
|
||||
let password = page_password(headers);
|
||||
let bearer = bearer_token(headers);
|
||||
if let Some(pad_id) = page.pad_id {
|
||||
if db::pad_public_page_unprotected(&state.db, pad_id).await? {
|
||||
return Ok(());
|
||||
@@ -358,10 +369,13 @@ async fn ensure_public_page_access(
|
||||
let pad = db::find_pad(&state.db, &slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
if db::verify_pad_password(&pad, password) {
|
||||
if has_header_resource_access(state, headers, "pad", &slug).await? {
|
||||
return Ok(());
|
||||
}
|
||||
if verify_resource_access_token(state, "pad", &slug, bearer).await? {
|
||||
let password_ok = db::verify_pad_password(&pad, password);
|
||||
check_resource_password_attempt(state, headers, "pad", &slug, password, password_ok)
|
||||
.await?;
|
||||
if password_ok {
|
||||
return Ok(());
|
||||
}
|
||||
return if pad.password_hash.is_some() {
|
||||
@@ -392,10 +406,20 @@ async fn ensure_public_page_access(
|
||||
let workspace = db::find_workspace(&state.db, &slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_workspace)?;
|
||||
if db::verify_workspace_password(&workspace, password) {
|
||||
if has_header_resource_access(state, headers, "workspace", &slug).await? {
|
||||
return Ok(());
|
||||
}
|
||||
if verify_resource_access_token(state, "workspace", &slug, bearer).await? {
|
||||
let password_ok = db::verify_workspace_password(&workspace, password);
|
||||
check_resource_password_attempt(
|
||||
state,
|
||||
headers,
|
||||
"workspace",
|
||||
&slug,
|
||||
password,
|
||||
password_ok,
|
||||
)
|
||||
.await?;
|
||||
if password_ok {
|
||||
return Ok(());
|
||||
}
|
||||
return if workspace.password_hash.is_some() {
|
||||
@@ -476,8 +500,9 @@ pub async fn pad_history(
|
||||
&state,
|
||||
&slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let revisions = db::list_pad_revisions(&state.db, pad.id)
|
||||
@@ -501,8 +526,9 @@ pub async fn pad_restore(
|
||||
&state,
|
||||
&slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let level = if db::verify_pad_password(&pad, payload.password.as_deref())
|
||||
@@ -514,7 +540,7 @@ pub async fn pad_restore(
|
||||
&state,
|
||||
"pad",
|
||||
&slug,
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -555,6 +581,7 @@ pub(super) async fn authorized_pad(
|
||||
password: Option<&str>,
|
||||
access_token: Option<&str>,
|
||||
bearer: Option<&str>,
|
||||
headers: &HeaderMap,
|
||||
) -> Result<db::Pad, ApiError> {
|
||||
let pad = db::find_pad(&state.db, slug)
|
||||
.await?
|
||||
@@ -563,11 +590,13 @@ pub(super) async fn authorized_pad(
|
||||
if pad.is_private != 0 && token_level == AccessLevel::None {
|
||||
return Err(ApiError::not_found_note());
|
||||
}
|
||||
if pad.password_hash.is_some()
|
||||
&& !db::verify_pad_password(&pad, password)
|
||||
&& token_level == AccessLevel::None
|
||||
{
|
||||
return Err(ApiError::forbidden("Password required or incorrect."));
|
||||
if pad.password_hash.is_some() && token_level < AccessLevel::Write {
|
||||
let password_ok = db::verify_pad_password(&pad, password);
|
||||
check_resource_password_attempt(state, headers, "pad", slug, password, password_ok)
|
||||
.await?;
|
||||
if token_level == AccessLevel::None && !password_ok {
|
||||
return Err(ApiError::forbidden("Password required or incorrect."));
|
||||
}
|
||||
}
|
||||
Ok(pad)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user