CrackedRuby logo

CrackedRuby

File Permissions

A comprehensive guide to managing file system permissions in Ruby, covering permission checking, modification, and security considerations.

Core Built-in Classes File and IO Classes
2.9.3

Overview

Ruby provides extensive file permission management through the File class and File::Stat objects. File permissions control access to files and directories using a three-tier system: owner, group, and others. Each tier has read, write, and execute permissions represented as octal values or symbolic constants.

The File class offers methods to check, modify, and analyze file permissions. Ruby represents permissions using octal notation (like 0644) or symbolic constants (File::RDWR, File::CREAT). The File::Stat class provides detailed permission information through bitmask operations and convenience methods.

Ruby's permission system works across Unix-like systems and Windows, though Windows has different underlying permission models. The interface remains consistent while Ruby handles platform-specific translation.

# Check if file is readable
File.readable?('/etc/passwd')
# => true

# Get detailed permission info
stat = File.stat('example.txt')
stat.mode.to_s(8)
# => "100644"

Permission operations involve checking current permissions, modifying them with chmod/chown, and handling permission-related errors. Ruby also provides methods to test specific permission combinations and create files with predetermined permissions.

# Create file with specific permissions
File.open('secure.txt', 'w', 0600) do |f|
  f.write("confidential data")
end

# Verify permissions were set correctly
File.stat('secure.txt').mode & 0777
# => 384 (0600 in decimal)

Basic Usage

File permission checking uses predicate methods that return boolean values. These methods test permissions for the current process's effective user and group IDs.

# Basic permission checks
File.readable?('config.yml')      # => true
File.writable?('logs/app.log')    # => true
File.executable?('scripts/deploy') # => true

# Check if file exists and has permissions
if File.readable?('data.json')
  content = File.read('data.json')
end

The File.stat method returns a File::Stat object containing comprehensive permission information. The mode attribute represents permissions as a bitmask combining file type and permission bits.

stat = File.stat('example.txt')

# Get full mode (includes file type bits)
stat.mode
# => 33188

# Get just permission bits
permissions = stat.mode & 0777
permissions.to_s(8)
# => "644"

# Check specific permission patterns
stat.mode & 0400 != 0  # owner read
# => true
stat.mode & 0200 != 0  # owner write  
# => true
stat.mode & 0100 != 0  # owner execute
# => false

File.chmod changes file permissions using octal notation. The method accepts either a single file path or multiple paths for batch operations.

# Change single file permissions
File.chmod(0755, 'script.rb')

# Change multiple files
File.chmod(0644, 'file1.txt', 'file2.txt', 'file3.txt')

# Verify the change
File.stat('script.rb').mode.to_s(8)
# => "100755"

Creating files with specific permissions uses the mode parameter in File.open or File.new. The umask affects the final permissions, so explicitly setting permissions after creation ensures accuracy.

# Create with permissions (affected by umask)
File.open('temp.txt', 'w', 0666) do |f|
  f.write("temporary data")
end

# Set exact permissions after creation
File.chmod(0666, 'temp.txt')

Error Handling & Debugging

Permission-related errors occur when operations exceed the current user's access rights. Ruby raises specific exception types that applications should handle appropriately.

The most common permission error is Errno::EACCES (Permission denied), raised when attempting unauthorized file operations. This exception occurs during file reading, writing, or permission modification.

begin
  File.open('/root/private.txt', 'r')
rescue Errno::EACCES => e
  puts "Access denied: #{e.message}"
  # Handle gracefully - log, use alternative, or request elevation
rescue Errno::ENOENT => e
  puts "File not found: #{e.message}"
end

Errno::EPERM occurs when attempting to change ownership or permissions without sufficient privileges. This typically happens when non-root users try to change file ownership or set special permission bits.

begin
  File.chown(0, 0, 'system_file.txt')
rescue Errno::EPERM => e
  puts "Operation not permitted: #{e.message}"
  # Log the attempt or request administrator action
rescue Errno::ENOENT => e
  puts "File does not exist: #{e.message}"
end

Permission debugging requires understanding the effective user ID, group memberships, and umask settings. These factors determine what operations the current process can perform.

# Debug current process permissions
puts "Effective UID: #{Process.euid}"
puts "Effective GID: #{Process.egid}"
puts "Groups: #{Process.groups}"

# Check umask (affects new file permissions)
old_umask = File.umask
puts "Current umask: #{old_umask.to_s(8)}"
File.umask(old_umask)  # Restore original

Testing permission scenarios requires creating files with known permissions and verifying behavior across different user contexts.

def test_permission_scenario(file_path, expected_mode)
  begin
    actual_mode = File.stat(file_path).mode & 0777
    if actual_mode == expected_mode
      puts "✓ Permissions correct: #{expected_mode.to_s(8)}"
    else
      puts "✗ Permission mismatch: expected #{expected_mode.to_s(8)}, got #{actual_mode.to_s(8)}"
    end
  rescue Errno::ENOENT
    puts "✗ File does not exist: #{file_path}"
  rescue Errno::EACCES
    puts "✗ Cannot access file: #{file_path}"
  end
end

Directory permission errors require understanding that directory traversal needs execute permission, while listing contents needs read permission.

# Directory permission debugging
def check_directory_access(dir_path)
  can_read = File.readable?(dir_path)
  can_write = File.writable?(dir_path)
  can_execute = File.executable?(dir_path)
  
  puts "Directory: #{dir_path}"
  puts "  Readable (can list): #{can_read}"
  puts "  Writable (can create files): #{can_write}" 
  puts "  Executable (can traverse): #{can_execute}"
  
  if can_execute && !can_read
    puts "  Warning: Can traverse but not list contents"
  elsif can_read && !can_execute
    puts "  Warning: Cannot access directory contents"
  end
end

Production Patterns

Production file permission management requires balancing security, functionality, and maintainability. Applications must handle permissions correctly across deployment environments while maintaining security boundaries.

Web applications typically run under restricted user accounts with specific permission requirements. Files must be readable by the application user while remaining secure from other system users.

class SecureFileManager
  def self.setup_application_files(app_root)
    # Set directory permissions for application structure
    File.chmod(0755, app_root)
    File.chmod(0755, File.join(app_root, 'public'))
    File.chmod(0700, File.join(app_root, 'tmp'))
    File.chmod(0700, File.join(app_root, 'log'))
    
    # Secure configuration files
    config_files = Dir.glob(File.join(app_root, 'config', '*.yml'))
    config_files.each { |f| File.chmod(0600, f) }
    
    # Make scripts executable
    script_files = Dir.glob(File.join(app_root, 'bin', '*'))
    script_files.each { |f| File.chmod(0755, f) }
  end
  
  def self.verify_permissions(app_root)
    issues = []
    
    # Check critical directories
    %w[tmp log].each do |dir|
      dir_path = File.join(app_root, dir)
      unless File.writable?(dir_path)
        issues << "#{dir} directory not writable"
      end
    end
    
    # Check configuration security
    config_files = Dir.glob(File.join(app_root, 'config', '*.yml'))
    config_files.each do |file|
      mode = File.stat(file).mode & 0777
      if mode & 0077 != 0  # Check group/other permissions
        issues << "#{file} has overly permissive permissions: #{mode.to_s(8)}"
      end
    end
    
    issues
  end
end

Log rotation and file cleanup require careful permission handling to prevent security issues and ensure continued operation.

class LogRotationManager
  def initialize(log_dir, app_user)
    @log_dir = log_dir
    @app_user = app_user
  end
  
  def rotate_logs
    log_files = Dir.glob(File.join(@log_dir, '*.log'))
    
    log_files.each do |log_file|
      next unless File.writable?(log_file)
      
      timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
      archived_name = "#{log_file}.#{timestamp}"
      
      # Rotate the file
      File.rename(log_file, archived_name)
      
      # Create new log file with proper permissions
      File.open(log_file, 'w', 0640) do |f|
        f.write("Log rotation at #{Time.now}\n")
      end
      
      # Compress old log
      system("gzip #{archived_name}")
    end
  end
  
  def cleanup_old_logs(days_to_keep = 30)
    cutoff_time = Time.now - (days_to_keep * 24 * 60 * 60)
    
    archived_logs = Dir.glob(File.join(@log_dir, '*.log.*.gz'))
    archived_logs.each do |log_file|
      if File.mtime(log_file) < cutoff_time
        File.unlink(log_file)
      end
    end
  end
end

Database backup security requires setting restrictive permissions on backup files while ensuring the backup process can execute successfully.

class DatabaseBackupManager
  def initialize(backup_dir)
    @backup_dir = backup_dir
    ensure_backup_directory
  end
  
  def create_backup(database_name)
    timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
    backup_file = File.join(@backup_dir, "#{database_name}_#{timestamp}.sql")
    
    # Create backup with restrictive permissions
    File.open(backup_file, 'w', 0600) do |f|
      # Database backup logic would write to f
      f.write("-- Backup created at #{Time.now}\n")
    end
    
    # Verify permissions were set correctly
    actual_mode = File.stat(backup_file).mode & 0777
    unless actual_mode == 0600
      File.chmod(0600, backup_file)  # Ensure security
    end
    
    backup_file
  end
  
  private
  
  def ensure_backup_directory
    unless File.directory?(@backup_dir)
      Dir.mkdir(@backup_dir)
    end
    
    # Secure backup directory
    File.chmod(0700, @backup_dir)
  end
end

Configuration management systems need to handle sensitive files with appropriate permissions while allowing automated deployment processes to function.

class ConfigurationDeployer
  def deploy_configuration(source_dir, target_dir)
    config_mapping = {
      'database.yml' => 0600,    # Database credentials
      'secrets.yml' => 0600,     # Application secrets
      'application.yml' => 0644, # General application config
      'nginx.conf' => 0644       # Web server config
    }
    
    config_mapping.each do |filename, permissions|
      source_file = File.join(source_dir, filename)
      target_file = File.join(target_dir, filename)
      
      if File.exist?(source_file)
        FileUtils.cp(source_file, target_file)
        File.chmod(permissions, target_file)
        
        puts "Deployed #{filename} with permissions #{permissions.to_s(8)}"
      else
        puts "Warning: #{filename} not found in source directory"
      end
    end
  end
end

Reference

File Permission Methods

Method Parameters Returns Description
File.readable?(path) path (String) Boolean Tests if file is readable by effective user
File.writable?(path) path (String) Boolean Tests if file is writable by effective user
File.executable?(path) path (String) Boolean Tests if file is executable by effective user
File.readable_real?(path) path (String) Boolean Tests readability using real user ID
File.writable_real?(path) path (String) Boolean Tests writability using real user ID
File.executable_real?(path) path (String) Boolean Tests executability using real user ID
File.world_readable?(path) path (String) Integer or nil Returns mode if world-readable, nil otherwise
File.world_writable?(path) path (String) Integer or nil Returns mode if world-writable, nil otherwise

File Modification Methods

Method Parameters Returns Description
File.chmod(mode, *paths) mode (Integer), paths (String) Integer Changes file permissions, returns number of files changed
File.chown(uid, gid, *paths) uid (Integer), gid (Integer), paths (String) Integer Changes file ownership
File.lchown(uid, gid, *paths) uid (Integer), gid (Integer), paths (String) Integer Changes symlink ownership
File.umask() None Integer Returns current umask
File.umask(mask) mask (Integer) Integer Sets umask, returns previous value

File::Stat Permission Methods

Method Parameters Returns Description
stat.mode None Integer Returns full file mode including type and permissions
stat.readable? None Boolean Tests if file is readable
stat.readable_real? None Boolean Tests readability with real user ID
stat.writable? None Boolean Tests if file is writable
stat.writable_real? None Boolean Tests writability with real user ID
stat.executable? None Boolean Tests if file is executable
stat.executable_real? None Boolean Tests executability with real user ID
stat.world_readable? None Integer or nil Returns permissions if world-readable
stat.world_writable? None Integer or nil Returns permissions if world-writable

Permission Constants

Constant Value Description
File::RDONLY 0 Read-only file access
File::WRONLY 1 Write-only file access
File::RDWR 2 Read-write file access
File::CREAT 512 Create file if it doesn't exist
File::EXCL 2048 Fail if file exists when creating
File::TRUNC 1024 Truncate file to zero length
File::APPEND 8 Append to file

Common Permission Modes

Mode Octal Binary Description
--- 0 000 No permissions
--x 1 001 Execute only
-w- 2 010 Write only
-wx 3 011 Write and execute
r-- 4 100 Read only
r-x 5 101 Read and execute
rw- 6 110 Read and write
rwx 7 111 Read, write, and execute

File Type Constants

Constant Description
File::FT_FIFO Named pipe (FIFO)
File::FT_CHARACTERSPECIAL Character special device
File::FT_DIRECTORY Directory
File::FT_BLOCKSPECIAL Block special device
File::FT_REGULARFILE Regular file
File::FT_LINK Symbolic link
File::FT_SOCKET Socket

Permission Bitmask Operations

# Extract permission components
mode = File.stat('file.txt').mode
permissions = mode & 0777          # Get permission bits only
owner_perms = (mode & 0700) >> 6   # Owner permissions (0-7)
group_perms = (mode & 0070) >> 3   # Group permissions (0-7)
other_perms = mode & 0007          # Other permissions (0-7)

# Test specific permissions
owner_read = (mode & 0400) != 0    # Owner can read
group_write = (mode & 0020) != 0   # Group can write
other_exec = (mode & 0001) != 0    # Others can execute

# Common permission combinations
mode & 0111 != 0  # Any execute permission
mode & 0222 != 0  # Any write permission
mode & 0444 != 0  # Any read permission

Common Exception Types

Exception Description
Errno::EACCES Permission denied
Errno::EPERM Operation not permitted
Errno::ENOENT No such file or directory
Errno::EISDIR Is a directory
Errno::ENOTDIR Not a directory
Errno::EROFS Read-only file system