Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/main-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ jobs:
- name: Install Poetry
run: pip3 install poetry

- name: Install Python dependencies
run: poetry install --with tokenserver-unit-tests,dev --no-interaction --no-ansi

- name: Python Format Check
run: poetry run ruff format --diff tools

Expand Down Expand Up @@ -152,7 +155,7 @@ jobs:
key: ${{ runner.os }}-rust-toolchain-${{ env.RUST_VERSION }}

- name: Set Rust toolchain
run: rustup default ${RUST_VERSION}
run: rustup toolchain install ${RUST_VERSION} --component rustfmt --component clippy --no-self-update && rustup default ${RUST_VERSION}

- name: Cache cargo-audit
id: cache-cargo-audit
Expand Down Expand Up @@ -209,7 +212,7 @@ jobs:
key: ${{ runner.os }}-rust-toolchain-${{ env.RUST_VERSION }}

- name: Set Rust toolchain
run: rustup default ${RUST_VERSION}
run: rustup toolchain install ${RUST_VERSION} --component rustfmt --component clippy --no-self-update && rustup default ${RUST_VERSION}

- name: Rust Clippy ${{ matrix.target }}
run: make clippy_${{ matrix.target }}
Expand Down Expand Up @@ -248,7 +251,7 @@ jobs:
${{ runner.os }}-cargo-release-${{ matrix.target }}-

- name: Set Rust toolchain
run: rustup default ${RUST_VERSION}
run: rustup toolchain install ${RUST_VERSION} --component rustfmt --component clippy --no-self-update && rustup default ${RUST_VERSION}

- name: Rust Clippy release ${{ matrix.target }}
run: make clippy_release_${{ matrix.target }}
Expand Down
37 changes: 35 additions & 2 deletions syncserver/src/web/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ impl HawkPayload {
80
};
let path = uri.path_and_query().ok_or(HawkErrorKind::MissingPath)?;
let expiry = if path.path().ends_with("/info/collections") {
let expiry = if is_info_collections_path(path.path()) {
0
} else {
Utc::now().timestamp() as u64
Expand All @@ -197,6 +197,25 @@ impl HawkPayload {
}
}

/// Matches only the legacy read-only `/1.5/{uid}/info/collections` endpoint,
/// requiring exactly four path segments. An exact match is needed because a
/// plain `/info/collections` suffix also matches the storage route
/// `/1.5/{uid}/storage/info/collections` (collection `info`, item
/// `collections`), which must not receive the same handling.
fn is_info_collections_path(path: &str) -> bool {
let mut segments = path.trim_start_matches('/').split('/');
matches!(
(
segments.next(),
segments.next(),
segments.next(),
segments.next(),
segments.next(),
),
(Some("1.5"), Some(uid), Some("info"), Some("collections"), None) if !uid.is_empty()
)
}

/// Helper function for [HMAC](https://tools.ietf.org/html/rfc2104) verification.
fn verify_hmac(info: &[u8], key: &[u8], expected: &[u8]) -> ApiResult<()> {
let mut hmac = Hmac::<Sha256>::new_from_slice(key)?;
Expand All @@ -208,7 +227,21 @@ fn verify_hmac(info: &[u8], key: &[u8], expected: &[u8]) -> ApiResult<()> {
mod tests {
use std::fmt::{self, Display, Formatter};

use super::{HawkPayload, Secrets};
use super::{HawkPayload, Secrets, is_info_collections_path};

#[test]
fn info_collections_exemption_matches_only_exact_route() {
// The legacy read-only endpoint is exempt.
assert!(is_info_collections_path("/1.5/123/info/collections"));
// The writable storage route shares the same suffix but must not match.
assert!(!is_info_collections_path(
"/1.5/123/storage/info/collections"
));
// Other storage items must not match.
assert!(!is_info_collections_path("/1.5/123/storage/bookmarks/abc"));
// A trailing slash or extra segment must not match.
assert!(!is_info_collections_path("/1.5/123/info/collections/"));
}

#[test]
fn valid_header() {
Expand Down