lib/terminalwire/client/entitlement.rb in terminalwire-0.1.17 vs lib/terminalwire/client/entitlement.rb in terminalwire-0.2.0

- old
+ new

@@ -1,214 +1,9 @@ require "pathname" module Terminalwire::Client + # Entitlements are the security boundary between the server and the client that lives on the client. + # The server might request a file or directory from the client, and the client will check the entitlements + # to see if the server is authorized to access the requested resource. module Entitlement - class Paths - class Permit - attr_reader :path, :mode - # Ensure the default file mode is read/write for owner only. This ensures - # that if the server tries uploading an executable file, it won't be when it - # lands on the client. - # - # Eventually we'll move this into entitlements so the client can set maximum - # permissions for files and directories. - MODE = 0o600 # rw------- - - # Constants for permission bit masks - OWNER_PERMISSIONS = 0o700 # rwx------ - GROUP_PERMISSIONS = 0o070 # ---rwx--- - OTHERS_PERMISSIONS = 0o007 # ------rwx - - # We'll validate that modes are within this range. - MODE_RANGE = 0o000..0o777 - - def initialize(path:, mode: MODE) - @path = Pathname.new(path).expand_path - @mode = convert(mode) - end - - def permitted_path?(path) - # This MUST be done via File.fnmatch because Pathname#fnmatch does not work. If you - # try changing this 🚨 YOU MAY CIRCUMVENT THE SECURITY MEASURES IN PLACE. 🚨 - File.fnmatch @path.to_s, File.expand_path(path), File::FNM_PATHNAME - end - - def permitted_mode?(value) - # Ensure the mode is at least as permissive as the permitted mode. - mode = convert(value) - - # Extract permission bits for owner, group, and others - owner_bits = mode & OWNER_PERMISSIONS - group_bits = mode & GROUP_PERMISSIONS - others_bits = mode & OTHERS_PERMISSIONS - - # Ensure that the mode doesn't grant more permissions than @mode in any class (owner, group, others) - (owner_bits <= @mode & OWNER_PERMISSIONS) && - (group_bits <= @mode & GROUP_PERMISSIONS) && - (others_bits <= @mode & OTHERS_PERMISSIONS) - end - - def permitted?(path:, mode: @mode) - permitted_path?(path) && permitted_mode?(mode) - end - - def serialize - { - location: @path.to_s, - mode: @mode - } - end - - protected - def convert(value) - mode = Integer(value) - raise ArgumentError, "The mode #{format_octet value} must be an octet value between #{format_octet MODE_RANGE.first} and #{format_octet MODE_RANGE.last}" unless MODE_RANGE.cover?(mode) - mode - end - - def format_octet(value) - format("0o%03o", value) - end - end - - include Enumerable - - def initialize - @permitted = [] - end - - def each(&) - @permitted.each(&) - end - - def permit(path, **) - @permitted.append Permit.new(path:, **) - end - - def permitted?(path, mode: nil) - if mode - find { |it| it.permitted_path?(path) and it.permitted_mode?(mode) } - else - find { |it| it.permitted_path?(path) } - end - end - - def serialize - map(&:serialize) - end - end - - class Schemes - include Enumerable - - def initialize - @permitted = Set.new - end - - def each(&) - @permitted.each(&) - end - - def permit(scheme) - @permitted << scheme.to_s - end - - def permitted?(url) - include? URI(url).scheme - end - - def serialize - @permitted.to_a.map(&:to_s) - end - end - - class Policy - attr_reader :paths, :authority, :schemes - - ROOT_PATH = "~/.terminalwire".freeze - - def initialize(authority:) - @authority = authority - @paths = Paths.new - - # Permit the domain directory. This is necessary for basic operation of the client. - @paths.permit storage_path - @paths.permit storage_pattern - - @schemes = Schemes.new - # Permit http & https by default. - @schemes.permit "http" - @schemes.permit "https" - end - - def root_path - Pathname.new(ROOT_PATH) - end - - def authority_path - root_path.join("authorities/#{authority}") - end - - def storage_path - authority_path.join("storage") - end - - def storage_pattern - storage_path.join("**/*") - end - - def serialize - { - authority: @authority, - schemes: @schemes.serialize, - paths: @paths.serialize, - storage_path: storage_path.to_s, - } - end - end - - class RootPolicy < Policy - AUTHORITY = "terminalwire.com".freeze - - # Ensure the binary stubs are executable. This increases the - # file mode entitlement so that stubs created in ./bin are executable. - BINARY_PATH_FILE_MODE = 0o755 - - def initialize(*, **, &) - # Make damn sure the authority is set to Terminalwire. - super(*, authority: AUTHORITY, **, &) - - # Now setup special permitted paths. - @paths.permit root_path - @paths.permit root_pattern - - # Permit terminalwire to grant execute permissions to the binary stubs. - @paths.permit binary_pattern, mode: BINARY_PATH_FILE_MODE - end - - # Grant access to the `~/.terminalwire/**/*` path so users can install - # terminalwire apps via `terminalwire install svbtle`, etc. - def root_pattern - root_path.join("**/*") - end - - # Path where the terminalwire binary stubs are stored. - def binary_path - root_path.join("bin") - end - - # Pattern for the binary path. - def binary_pattern - binary_path.join("*") - end - end - - def self.resolve(*, authority:, **, &) - case authority - when RootPolicy::AUTHORITY - RootPolicy.new(*, **, &) - else - Policy.new *, authority:, **, & - end - end end end