Skip to content

Feature/filesystem mapping#2309

Closed
KesterTan wants to merge 44 commits intomasterfrom
feature/filesystem-mapping
Closed

Feature/filesystem mapping#2309
KesterTan wants to merge 44 commits intomasterfrom
feature/filesystem-mapping

Conversation

@KesterTan
Copy link
Copy Markdown
Contributor

This PR provides a way to map the current files and folders in Autolab to a specified user group associated with the instructor's email. This PR also allows users to provide SSH keys so that they can SSH to the server hosting Autolab.

Instructors should not be able to see courses that they do not manage, but they should be able to access and view courses that they do manage.

How Has This Been Tested?

Login to the gamma instance and create a course. Upload your ssh key by clicking your profile, then your account and then manage ssh keys. SSH to the instance and check that you can only access your course and not other courses.

Screenshot 2026-02-02 at 4 54 23 PM

Types of changes

  • [] Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have run rubocop and erblint for style check. If you haven't, run overcommit --install && overcommit --sign to use pre-commit hook for linting
  • My change requires a change to the documentation, which is located at Autolab Docs
  • I have updated the documentation accordingly, included in this PR

Futurecomputerfixer and others added 30 commits October 21, 2025 13:02
- Add SshKey model with validation and fingerprinting
- Implement UnixGroupManager service for user/group management
- Create Unix users/groups automatically on course/staff creation
- Add SSH key provisioning to authorized_keys
- Implement FilesystemEnforcer with group-based permissions
- Add SSH key management UI (restricted to staff/instructors)
- Fix require paths for services in controllers
- Add services directory to autoload paths
- Create bootstrap script for existing courses
- Add host system user setup scripts
- Support BusyBox useradd (use -p '*' instead of --disabled-password)
- Add comprehensive setup and testing scripts
@KesterTan KesterTan requested a review from jhs-panda February 2, 2026 21:55
file.write(input_file.read)
end
dest = absolute_path.join(input_file.original_filename)
File.open(dest, 'wb') { |f| f.write(input_file.read) }

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a
user-provided value
.

Copilot Autofix

AI 3 months ago

General approach: ensure that all paths and filenames derived from user input are (a) constrained to live under BASE_DIRECTORY, and (b) sanitized so they cannot contain directory separators, excessive dots, or other dangerous characters. We will treat params[:path] as a logical path within BASE_DIRECTORY, normalize and validate it before calling safe_expand_path, and we will sanitize both params[:name] and input_file.original_filename before using them to create folders or files. This addresses both variants CodeQL reported.

Concrete fix in this file:

  1. Add two helper methods in the private section:
    • sanitize_rel_path(path) which:
      • Forces the path into a relative, normalized form using Pathname#cleanpath.
      • Rejects any path that is absolute or that contains ".." segments.
    • sanitize_filename(name) which:
      • Uses a simple whitelist regex to restrict filenames to alphanumerics, underscore, dash, dot, and space.
      • Optionally collapses or strips leading dots and prevents blank results.
  2. Use sanitize_rel_path in safe_expand_path so that path from user input is cleaned before joining with BASE_DIRECTORY. If sanitization fails, raise ActionController::RoutingError, 'Not Found' or ArgumentError.
  3. Use sanitize_filename for:
    • params[:name] when creating a directory (dir = "#{absolute_path}/#{params[:name]}").
    • input_file.original_filename when calculating the destination file (dest = absolute_path.join(input_file.original_filename)).
      Keep using the sanitized versions consistently for both existence checks and path creation, so the behavior (duplicate checking, etc.) is preserved but now based on safe names.

We only touch app/controllers/file_manager_controller.rb within the provided snippets, adding the helper methods in the private section and updating a few lines in upload_file and safe_expand_path. No new external gem is necessary; we can rely on Ruby’s Pathname and simple regexes.


Suggested changeset 1
app/controllers/file_manager_controller.rb

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/app/controllers/file_manager_controller.rb b/app/controllers/file_manager_controller.rb
--- a/app/controllers/file_manager_controller.rb
+++ b/app/controllers/file_manager_controller.rb
@@ -151,27 +151,29 @@
       if check_instructor(absolute_path) && !params[:name].nil?
         all_filenames = Dir.entries(absolute_path)
         if params[:name] != ""
-          if all_filenames.include?(params[:name].to_s)
-            raise "File with name #{input_file.original_filename} already exists."
+          sanitized_dir_name = sanitize_filename(params[:name])
+          if all_filenames.include?(sanitized_dir_name)
+            raise "File or directory with name #{sanitized_dir_name} already exists."
           end
 
           # Creating a folder
-          dir = "#{absolute_path}/#{params[:name]}"
+          dir = absolute_path.join(sanitized_dir_name)
           FileUtils.mkdir_p(dir)
-          FilesystemEnforcer.fix_path(dir)
+          FilesystemEnforcer.fix_path(dir.to_s)
 
         else
           # Uploading a file
           input_file = params[:file]
           return unless input_file
-          if all_filenames.include?(input_file.original_filename)
-            raise "File with name #{input_file.original_filename} already exists."
+          sanitized_filename = sanitize_filename(input_file.original_filename)
+          if all_filenames.include?(sanitized_filename)
+            raise "File with name #{sanitized_filename} already exists."
           elsif input_file.size >= 1.gigabyte
             raise "File size is too large. Upload a file that is smaller than 1 GB."
           else
-            dest = absolute_path.join(input_file.original_filename) 
+            dest = absolute_path.join(sanitized_filename)
             File.open(dest, 'wb') { |f| f.write(input_file.read) }
-            FilesystemEnforcer.fix_path(dest.to_s)          
+            FilesystemEnforcer.fix_path(dest.to_s)
           end
         end
       else
@@ -183,6 +172,33 @@
 
 private
 
+  def sanitize_rel_path(path)
+    rel = (path || "").to_s
+    # Normalize path (remove redundant ".", "..", and slashes)
+    clean = Pathname.new(rel).cleanpath.to_s
+    # Disallow absolute paths
+    if clean.start_with?(File::SEPARATOR) || clean.start_with?("\\")
+      raise ActionController::RoutingError, 'Not Found'
+    end
+    # After normalization, reject any remaining parent-directory traversal
+    if clean.split(/[\\\/]/).include?("..")
+      raise ActionController::RoutingError, 'Not Found'
+    end
+    clean
+  end
+
+  def sanitize_filename(name)
+    raw = (name || "").to_s
+    # Allow only a safe subset of characters in filenames
+    safe = raw.gsub(/[^0-9A-Za-z.\- _]/, "_")
+    # Prevent empty or dot-only names
+    safe = safe.sub(/\A\.+/, "")
+    if safe.empty?
+      raise ActionController::RoutingError, 'Not Found'
+    end
+    safe
+  end
+
   def populate_directory(current_directory, current_url)
     directory = Dir.entries(current_directory)
     new_url = current_url == '/' ? '' : current_url
@@ -225,7 +241,9 @@
 
   def safe_expand_path(path)
     current_directory = Pathname.new(BASE_DIRECTORY)
-    tested_path = Pathname.new(File.join(BASE_DIRECTORY, path))
+    # Sanitize the relative path coming from user input to prevent directory traversal
+    sanitized_relative = sanitize_rel_path(path)
+    tested_path = Pathname.new(File.join(BASE_DIRECTORY, sanitized_relative))
     unless Archive.in_dir?(tested_path, current_directory, strict: false)
       raise ArgumentError, 'Should not be parent of root'
     end
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread app/services/unix_group_manager.rb Fixed
@KesterTan KesterTan force-pushed the feature/filesystem-mapping branch from 60c2994 to 4367517 Compare February 16, 2026 06:13
@KesterTan KesterTan closed this Apr 15, 2026
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.

4 participants