Skip to content

Conversation

@panarch
Copy link
Member

@panarch panarch commented Oct 21, 2025

Summary

  • Add a dedicated CLI option (--allowed-directory) so the proxy server can run in a restricted mode.
  • Preload the allowed directory subtree into an in-memory allowlist and gate every write request against it.
  • Keep the allowlist in sync by updating it after successful create/move/delete operations.

Usage Scenario

  1. Start the proxy once in unrestricted mode (e.g. glues server file --path /path/to/notebook). Use an API call such as RootId or FetchDirectories to capture the directory_id you want to expose, then stop the server.
  2. Relaunch the proxy in restricted mode:
    glues server file --path /path/to/notebook --allowed-directory <directory-id> [--auth-token <token>] [--listen <addr>]
  3. Connect the client using the proxy backend (enter the server URL and optional token).
    • The client sees the specified directory as the virtual root.
    • Any attempt to reach sibling or parent directories—or to write outside the subtree—returns a 403 error with outside the allowed subtree.
  4. Create, move, or delete notes/directories within the subtree as usual; the allowlist updates automatically so subsequent requests see the new state.

Summary by CodeRabbit

  • New Features

    • Added ability to restrict proxy server access to a specific directory and its subtree, with enforced read/write limitations at that scope level.
  • Documentation

    • Updated README with guidance on the new directory restriction feature.
  • Tests

    • Added access control validation test suite.

@coderabbitai
Copy link

coderabbitai bot commented Oct 21, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Add restricted proxy mode with directory allowlist" directly and accurately describes the main objective of the changeset. The PR implements a new CLI option --allowed-directory that enables restricted proxy server mode by enforcing subtree-restricted access control through an in-memory allowlist. The title is concise, specific, and avoids vague terminology, clearly conveying to reviewers that this PR adds new access-control functionality rather than modifying existing behavior or addressing bug fixes.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch proxy-allowlist

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +116 to +118
}
Log { .. } | Sync => GuardEvaluation::allow(),
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject Sync requests when proxy is restricted

When an allowed directory is configured the proxy is supposed to block all reads and writes outside that subtree, but ProxyAccess::evaluate still unconditionally allows ProxyRequest::Sync. A Sync job invokes the backend’s global synchronization routine (ProxyServer::handledb.sync_job()), which updates the entire notebook regardless of directory boundaries. A restricted client can therefore trigger writes that touch directories outside the allowlisted subtree, contradicting the README’s promise that “reads and writes outside the subtree are rejected”. Consider rejecting Sync (or scoping it) whenever an allowlist is active.

Useful? React with 👍 / 👎.

@gluesql-coverage-bot
Copy link

gluesql-coverage-bot bot commented Oct 21, 2025

Glues Coverage Report

  • Commit: 059369dcbfac774f6fad254df2f63c5b84aafcf4
  • Timestamp: 2025-10-21T050713Z
  • Report: View report

@panarch panarch self-assigned this Oct 21, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (12)
README.md (1)

56-56: Clarify how to obtain and add a quick example.

Suggest adding 1–2 sentences on how users can get a directory ID (e.g., via RootId and navigations, or from the TUI/devtools) and a concrete startup example to reduce ambiguity.

server/tests/proxy_access.rs (1)

48-181: Add a positive-read and a “no allowlist” control test.

  • Include one success path that reads note content inside the allowed subtree to validate allowed reads (not only writes/mutations).
  • Add a small test with allowed_directory=None to ensure the guard is bypassed and existing behavior remains unchanged.

Example additions (new tests):

#[tokio::test]
async fn proxy_allows_reads_within_allowed_subtree() {
    // Arrange similar to main test but: create note under `allowed_dir`,
    // then assert FetchNoteContent returns Ok.
}

#[tokio::test]
async fn proxy_without_allowlist_is_unrestricted() {
    let db = Db::memory().await.unwrap();
    let proxy = glues_core::backend::proxy::ProxyServer::new(Box::new(db));
    let state = ServerState::new(proxy, None).await.unwrap();
    // Assert that e.g., RootId equals the real root and outside ops are permitted.
}
server/src/args.rs (1)

12-13: Avoid accepting an empty auth token.

If GLUES_SERVER_TOKEN or --auth-token "" is provided, the server will enforce a blank Bearer token. Reject empty values at parse-time.

Apply this diff:

-use {
-    clap::{Args, Parser, Subcommand},
+use {
+    clap::{Args, Parser, Subcommand, builder::NonEmptyStringValueParser},
     glues_core::types::DirectoryId,
     std::net::SocketAddr,
 };
@@
-    #[arg(long, env = "GLUES_SERVER_TOKEN")]
+    #[arg(long, env = "GLUES_SERVER_TOKEN", value_parser = NonEmptyStringValueParser::new())]
     pub auth_token: Option<String>,

Optionally, also derive Debug for easier diagnostics:

-#[derive(Clone, Args)]
+#[derive(Clone, Debug, Args)]
 pub struct ServerArgs { ... }
@@
-#[derive(Subcommand, Clone)]
+#[derive(Subcommand, Clone, Debug)]
 pub enum StorageCommand { ... }
server/src/state.rs (2)

63-76: Global request serialization may throttle throughput.

self.server: AsyncMutex<ProxyServer> serializes all requests, including reads. If feasible, consider:

  • Making ProxyServer internally synchronized so handle takes &self.
  • Narrowing the lock to only operations that require atomicity (e.g., the collect-removal + remove sequence), letting read ops proceed concurrently.

47-56: Add a debug log on access denial.

Helps operators understand why a request was blocked.

-                GuardEvaluation::Deny { message } => {
-                    return ProxyResponse::Err(message);
-                }
+                GuardEvaluation::Deny { message } => {
+                    tracing::debug!(%message, "proxy access denied");
+                    return ProxyResponse::Err(message);
+                }
server/src/lib.rs (1)

55-60: Optional: restrict CORS origins via env for deployments.

Current Any is convenient but permissive. Consider allowing a comma‑separated GLUES_CORS_ORIGINS to tighten origins in production.

Sketch:

let cors = if let Ok(list) = std::env::var("GLUES_CORS_ORIGINS") {
    let origins = list.split(',').map(|s| s.trim().parse().unwrap()).collect::<Vec<_>>();
    CorsLayer::new().allow_origin(origins)
                    .allow_methods(Any).allow_headers(Any)
} else {
    CorsLayer::new().allow_origin(Any)
                    .allow_methods(Any).allow_headers(Any)
};
server/src/proxy_access.rs (6)

223-248: Make removal traversal cycle-safe and de-duplicate entries

If directory relations are ever corrupted (cycles, shared subtrees), this loop can revisit nodes indefinitely and duplicate IDs. Add visited sets to prevent infinite traversal and duplicate removals.

Please confirm the DB guarantees a strict tree; if not, apply the patch below.

 pub(crate) async fn collect_removal_plan(
     server: &mut ProxyServer,
     directory_id: DirectoryId,
 ) -> Result<RemovalPlan> {
-    let mut directories = Vec::new();
-    let mut notes = Vec::new();
-    let mut queue = VecDeque::new();
+    let mut directories = Vec::new();
+    let mut notes = Vec::new();
+    let mut queue = VecDeque::new();
+    let mut seen_dirs: HashSet<DirectoryId> = HashSet::new();
+    let mut seen_notes: HashSet<NoteId> = HashSet::new();
 
-    queue.push_back(directory_id.clone());
+    seen_dirs.insert(directory_id.clone());
+    queue.push_back(directory_id.clone());
 
     while let Some(dir_id) = queue.pop_front() {
         directories.push(dir_id.clone());
 
         let children = server.db.fetch_directories(dir_id.clone()).await?;
         for child in children {
-            queue.push_back(child.id.clone());
+            if seen_dirs.insert(child.id.clone()) {
+                queue.push_back(child.id.clone());
+            }
         }
 
         let dir_notes = server.db.fetch_notes(dir_id).await?;
         for note in dir_notes {
-            notes.push(note.id);
+            if seen_notes.insert(note.id.clone()) {
+                notes.push(note.id);
+            }
         }
     }
 
     Ok(RemovalPlan { directories, notes })
 }

149-159: Avoid relying on Display for IDs in error messages

Formatting with {id} requires Display for DirectoryId/NoteId; using {:?} (Debug) is safer across types and avoids build breaks if Display isn’t implemented.

If Display is guaranteed, feel free to ignore; otherwise apply:

     pub(crate) fn deny_directory(id: &DirectoryId) -> GuardEvaluation {
         GuardEvaluation::Deny {
-            message: format!("proxy: directory {id} is outside the allowed subtree"),
+            message: format!("proxy: directory {id:?} is outside the allowed subtree"),
         }
     }
 
     pub(crate) fn deny_note(id: &NoteId) -> GuardEvaluation {
         GuardEvaluation::Deny {
-            message: format!("proxy: note {id} is outside the allowed subtree"),
+            message: format!("proxy: note {id:?} is outside the allowed subtree"),
         }
     }

179-190: Bridge PendingMutation to PostPlan for directory removal

GuardEvaluation can carry a pending CollectRemoval, but PostPlan::from_request doesn’t cover RemoveDirectory. Add a helper to convert the pending to a concrete RemovalPlan so callers don’t have to stitch this together.

I can wire state.rs to use this if helpful.

 impl PostPlan {
     pub(crate) fn from_request(request: &ProxyRequest) -> Self {
         match request {
             ProxyRequest::AddDirectory { .. } => PostPlan::AddDirectory,
             ProxyRequest::AddNote { .. } => PostPlan::AddNote,
             ProxyRequest::RemoveNote { note_id } => PostPlan::RemoveNote {
                 note_id: note_id.clone(),
             },
             _ => PostPlan::None,
         }
     }
 }
+
+impl PostPlan {
+    pub(crate) async fn from_guard(
+        server: &mut ProxyServer,
+        eval: &GuardEvaluation,
+    ) -> Result<Self> {
+        match eval {
+            GuardEvaluation::Allow {
+                pending: Some(PendingMutation::CollectRemoval { directory_id }),
+            } => {
+                let plan = collect_removal_plan(server, directory_id.clone()).await?;
+                Ok(PostPlan::RemoveDirectory(plan))
+            }
+            _ => Ok(PostPlan::None),
+        }
+    }
+}

197-198: Minor: drop the let _ = no-op binding

You can just await the call for existence validation; binding to _ is unnecessary.

-    let _ = server.db.fetch_directory(root.clone()).await?;
+    server.db.fetch_directory(root.clone()).await?;

208-212: Reduce unnecessary cloning when enqueuing child directories

Move the ID once and clone only for the set insert, avoiding the second clone.

-        for child in children {
-            if directories.insert(child.id.clone()) {
-                queue.push_back(child.id.clone());
-            }
-        }
+        for child in children {
+            let id = child.id;
+            if directories.insert(id.clone()) {
+                queue.push_back(id);
+            }
+        }

274-284: Keep allowed_root invariant after removals (debug check)

As a guardrail, assert the allowed_root remains tracked even after a removal plan is applied. This catches accidental root removal plans early.

         PostPlan::RemoveDirectory(RemovalPlan { directories, notes }) => {
             if matches!(response, ProxyResponse::Ok(ResultPayload::Unit)) {
                 let mut guard = access.write().await;
                 for directory_id in directories {
                     guard.directories.remove(&directory_id);
                 }
                 for note_id in notes {
                     guard.notes.remove(&note_id);
                 }
+                debug_assert!(
+                    guard.directories.contains(&guard.allowed_root),
+                    "allowed_root must remain in the allowed directories set"
+                );
             }
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9205cb and 0f9cc53.

📒 Files selected for processing (6)
  • README.md (1 hunks)
  • server/src/args.rs (1 hunks)
  • server/src/lib.rs (5 hunks)
  • server/src/proxy_access.rs (1 hunks)
  • server/src/state.rs (1 hunks)
  • server/tests/proxy_access.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Run cargo clippy --all-targets -- -D warnings and ensure it passes
Run cargo fmt across the workspace before committing

Files:

  • server/src/state.rs
  • server/src/proxy_access.rs
  • server/src/args.rs
  • server/tests/proxy_access.rs
  • server/src/lib.rs
🧬 Code graph analysis (4)
server/src/state.rs (1)
server/src/proxy_access.rs (5)
  • apply_post_plan (249-286)
  • collect_removal_plan (223-248)
  • load_proxy_access (192-221)
  • new (25-35)
  • from_request (180-189)
server/src/proxy_access.rs (1)
server/src/state.rs (1)
  • new (23-32)
server/tests/proxy_access.rs (2)
server/src/proxy_access.rs (1)
  • new (25-35)
server/src/state.rs (1)
  • new (23-32)
server/src/lib.rs (2)
server/src/args.rs (1)
  • parse_args (47-49)
server/src/state.rs (1)
  • new (23-32)
🔇 Additional comments (5)
server/tests/proxy_access.rs (1)

12-46: Helper assertions read well and keep the test focused.

The expect_* utilities are concise and make failures actionable. LGTM.

server/src/lib.rs (3)

46-49: Nice state wiring and router setup.

Backend construction, state init with allowed_directory, and axum wiring look clean. LGTM.

Also applies to: 75-83


107-113: Handler is straightforward and correct for the established JSON envelope.

Fine to always return 200 with a typed ProxyResponse. LGTM.


1-128: Manual verification required: cargo toolchain not accessible in sandbox environment.

The sandbox environment lacks necessary system resources (/proc filesystem) to execute cargo commands. Cargo fmt and clippy verification must be performed locally on your machine before merge.

Please run these commands in your local repository and confirm they pass:

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings

Ensure both commands complete without errors or warnings before merging.

server/src/proxy_access.rs (1)

37-118: ACL evaluation logic is clear and consistent

Guards comprehensively cover fetch/mutate operations, root protection looks correct, and note/directory checks are symmetric. Nice.

Comment on lines 39 to 44
let ServerArgs {
listen,
auth_token,
allowed_directory,
storage,
} = args;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Treat empty auth tokens as “no auth” to avoid enforcing Bearer (blank).

Filter out empty/whitespace-only tokens at startup.

Apply this diff:

     let ServerArgs {
         listen,
-        auth_token,
+        auth_token,
         allowed_directory,
         storage,
     } = args;
 
-    let backend = build_backend(storage).await?;
+    // Treat empty or whitespace-only tokens as None.
+    let auth_token = auth_token.and_then(|t| {
+        let trimmed = t.trim().to_owned();
+        if trimmed.is_empty() { None } else { Some(trimmed) }
+    });
+
+    let backend = build_backend(storage).await?;

No behavior change needed below; the existing if let Some(token) branch will now skip for empty inputs.

Also applies to: 61-69

🤖 Prompt for AI Agents
In server/src/lib.rs around lines 39-44 (and similarly 61-69), the extracted
auth_token should treat empty or whitespace-only strings as None so the existing
if let Some(token) branch won't enforce a blank "Bearer " header; modify the
initialization to trim the auth_token and set it to None when it's empty or only
whitespace (e.g., map Some(s) to s.trim().is_empty() ? None :
Some(s.trim().to_string())), ensuring downstream logic remains unchanged.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
README.md (1)

56-56: Well-documented feature addition. Line 56 clearly explains the new --allowed-directory flag, including how to obtain a directory ID and the expected behavior (virtual root, access rejection, root ID exposure). Placement within the Proxy section is logical and accessible to users.

Optional enhancement: For developers troubleshooting access issues, consider mentioning that out-of-subtree requests return a 403 error. This is a nice-to-have for future documentation polish and could be added to help users understand expected error behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f9cc53 and 059369d.

📒 Files selected for processing (1)
  • README.md (1 hunks)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants