Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Facl fixes #26

Merged
merged 15 commits into from
Sep 5, 2018
13 changes: 8 additions & 5 deletions app/controllers/mappings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

class MappingsController < ApplicationController
def index
@directory_permissions_command = Mapping.directory_permissions_command
end


Expand All @@ -18,18 +19,20 @@ def create
flash[:success] = @mapping.save_message
redirect_to action: :index
else
flash[:warning] = 'Unable to create new mapping. ' + @mapping.save_message
flash[:warning] = 'Unable to create new mapping. ' + @mapping.format_error_messages
redirect_to new_mapping_path, locals: params
end
end

# POST /mappings
def destroy
if Mapping.destroy_and_remove_facls(params[:id])
flash[:success] = 'Successfully deleted mapping.'
def destroy
mapping = Mapping.get_mapping_for_id(params[:id])

if mapping.destroy_and_remove_facls()
flash[:success] = mapping.save_message
redirect_to action: :index
else
flash[:danger] = 'Unable to delete mapping ' + params[:id]
flash[:danger] = mapping.save_message
redirect_to action: :index
end
end
Expand Down
98 changes: 79 additions & 19 deletions app/models/mapping.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
require 'etc'
require 'fileutils'
require 'ood_support'
require 'ostruct'
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't need this anymore if you don't use it anywhere.

require 'pathname'
require 'yaml/store'


class Mapping < ActiveRecord::Base
attr_accessor :save_message
validates :user, :app, :dataset, presence: true
validates :user, :app, :dataset, presence: { message: "A %{attribute} must be selected." }
validate :dataset_path_must_exist
validate :app_path_may_not_be_blank
validates_uniqueness_of :user, scope: [:user, :app], message: "Unable to create a second mapping between user and app."

def initialize(params)
super(params)

@save_message = ''
end

# Type dataset as a Pathname
def dataset
Pathname.new(super)
super ? Pathname.new(super): Pathname.new('')
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. You can accomplish the same thing doing this:

    Pathname.new(super.to_s)

    since nil.to_s == ''

  2. Pathname.new('') is not a great "null object". This is a difficult thing with Ruby that I don't have a good answer to right now. Notice:

irb(main):012:0> Pathname.new('').to_s
=> ""
irb(main):013:0> Pathname.new('').exist?
=> false
irb(main):014:0> Pathname.new('').file?
=> false
irb(main):015:0> Pathname.new('').directory?
=> false
irb(main):016:0> Pathname.new('').expand_path
=> #<Pathname:/Users/efranz>
irb(main):016:0> Pathname.new('').expand_path.exist?
=> true
irb(main):025:0> Pathname.new('').expand_path.directory?
=> true

And I might get clever and suggest Pathname.new('/dev/null') but then this has its own problems:

irb(main):022:0> Pathname.new('/dev/null').expand_path
=> #<Pathname:/dev/null> # good!
irb(main):018:0> Pathname.new('/dev/null').directory?
=> false  # good!
irb(main):019:0> Pathname.new('/dev/null').file?
=> false  # good!
irb(main):020:0> Pathname.new('/dev/null').exist?
=> true  # bad!

Of course, nil is normally not desired, because then you have to add guard clauses everywhere and nil is generally a bad idea. But in this case I wonder if we should use it for the "null object", unless there is a sensible null object for Pathname that can be produced (maybe we could make our own? - like Pathname.new('') and then override expand_path and other methods that return values we don't like).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was looking for a default constructor or invalid test for Pathname. Using /dev/null is a nice idea, except for its perfectly correct exist? behavior.

end

# Type app as a Pathname
def app
Pathname.new(super)
super ? Pathname.new(super): Pathname.new('')
end

# @return [Array<String>]
Expand Down Expand Up @@ -55,28 +63,29 @@ def to_hash
end

# Custom destructor
def self.destroy_and_remove_facls(id)
def destroy_and_remove_facls
begin
mapping = find(id)
mapping.remove_rx_facl(mapping.dataset)
mapping.remove_rx_facl(mapping.app)
mapping.destroy
dump_to_yaml
remove_rx_facl(dataset)
remove_rx_facl(app)
destroy
Mapping.dump_to_yaml
@save_message = 'Mapping successfully destroyed.'

return true
rescue OodSupport::InvalidPath, OodSupport::BadExitCode => e
@save_message = 'Unable to destroy mapping because ' + e.to_s

return false
rescue ActiveRecord::RecordNotFound # User is probably mashing the delete
return false
end
end

# Create a user readable string from the error.messages hash
def format_error_messages
@save_message = errors.messages.map {|_, message| message}.join(' ')
@save_message = errors.messages.map do |key, message|
return message.join(' ') unless key == :base

message
end.join(' ')
end

# Custom save method
Expand All @@ -95,10 +104,6 @@ def save_and_set_facls
@save_message = 'Mapping successfully created.'

return true
rescue ActiveRecord::RecordNotUnique => e
@save_message = "Unable to create duplicate mapping between #{user}, #{app}, and #{dataset}"

return false
rescue OodSupport::InvalidPath, OodSupport::BadExitCode => e
@save_message = "Unable to set FACLS because " + e.to_s

Expand Down Expand Up @@ -126,13 +131,13 @@ def self.permission_sensitive_dirs

# @return [String]
def self.directory_permissions_command
permission_sensitive_dirs.map{|directory| "chmod 0775 #{directory.to_s}"}.join(' && ')
permission_sensitive_dirs.map{|directory| "chmod 2775 #{directory.to_s}"}.join(' && ')
end

# @return [Boolean]
def should_add_facl?(pathname)
# Calling owned first protects rx_facl_exists? from throwing InvalidPath
pathname.owned? && ! rx_facl_exists?(pathname)
Mapping.can_modify_facl?(pathname) && ! rx_facl_exists?(pathname)
end

# Idempotently add a RX entry to the ACL for a file
Expand All @@ -141,6 +146,8 @@ def add_rx_facl(pathname)

entry = build_facl_entry_for_user(user, Configuration.facl_user_domain)
OodSupport::ACLs::Nfs4ACL.add_facl(path: pathname, entry: entry)

logger.debug "add_rx_facl(#{pathname}) succeeded for #{user} and #{pathname.to_s}"
end

# Check if pathname / user combination occurs once or less in the database
Expand All @@ -154,7 +161,7 @@ def pathname_uniq_for_user?(pathname)

# @return [Boolean]
def should_remove_facl?(pathname)
pathname.owned? && rx_facl_exists?(pathname) && pathname_uniq_for_user?(pathname)
Mapping.can_modify_facl?(pathname) && rx_facl_exists?(pathname) && pathname_uniq_for_user?(pathname)
end

# Conditionally remove RX FACLs
Expand All @@ -166,6 +173,8 @@ def remove_rx_facl(pathname)

entry = build_facl_entry_for_user(user, Configuration.facl_user_domain)
OodSupport::ACLs::Nfs4ACL.rem_facl(path: pathname, entry: entry)

logger.debug "remove_rx_facl(#{pathname}) succeeded for #{user} and #{pathname.to_s}"
end

# Build FACL for user and domain combination
Expand Down Expand Up @@ -221,9 +230,60 @@ def dataset_path_must_exist
errors.add(:base, "Dataset must exist.") unless dataset.exist?
end

# Validator for app
def app_path_may_not_be_blank
errors.add(:base, "You must select an app.") unless app.exist?
end

# Directories between $HOME and the configured database directory must be set to 775
# @return [Boolean]
def self.directory_perms_are_775?(directory)
directory.stat.mode.to_s(8).end_with?('775')
end

# Checks to see if FACLs are modifiable by the user
#
# Note that this check assumes that no negative permissions have been set.
#
# @return [Boolean]
def self.can_modify_facl?(pathname)
pathname.exist? && ( pathname.owned? || group_facl_entry_has_C_set?(pathname) )
end

# Get the group owner's name
# @return [String]
def self.get_groupname_for_pathname(pathname)
Etc.getgrgid(pathname.stat.gid).name
end

# Check if pathname is owned by the admin_group
# @return [Boolean]
def self.app_has_correct_group_ownership?(pathname)
get_groupname_for_pathname(pathname) == Configuration.admin_group
end

# Does the app have permission modification enabled for the GROUP principle
# @return [Boolean]
def self.group_facl_entry_has_C_set?(pathname)
result = OodSupport::ACLs::Nfs4ACL.get_facl(
path: pathname
).entries.select{
|entry| entry.principle == 'GROUP'
}.first.permissions.include?(:C)

logger.debug "group_facl_entry_has_C_set?(#{pathname}) == #{result}"

result
end

def self.get_mapping_for_id(id)
begin
return Mapping.find(id)
rescue ActiveRecord::RecordNotFound
OpenStruct.new(
:destroy_and_remove_facls => false,
:save_message => "No mapping exists with id #{id}"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of having this, why can't we just do in the controller:

mapping = Mapping.find(params[:id])

and in the controller handle the ActiveRecord::RecordNotFound. Is it necessary to handle the ActiveRecord::RecordNotFound, which results in a 404?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One situation where they would see it is if there is a delay between submitting the first delete and immediately sending another. I know quite a few people who click buttons more than once.

I agree that the OpenStruct usage isn't ideal, it's just something that I was playing with on Friday. I liked that it let me avoid having to add an error handler for RecordNotFound.

Copy link
Contributor

Choose a reason for hiding this comment

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

As far as the multiple button clicks, the presence of the alert should prevent this from occurring:

<td><%= link_to 'Destroy', mapping_path(mapping), method: :delete, class: 'btn btn-danger', data: { confirm: 'Are you sure you want to delete this mapping?' } %></td>

A single click presents the confirmation alert, and only if that is "okay" does the delete request get sent.

Copy link
Contributor

Choose a reason for hiding this comment

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

And of course as we discussed offline, I think its appropriate to not add rescue for RecordNotFound since Rails should handle this to be 404.

end
end
end
4 changes: 2 additions & 2 deletions app/views/mappings/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<% if Mapping.has_directory_permission_errors? %>
<div class="alert alert-danger" role="alert">
<div class="panel-body">Warning: certain directories owned by <code>bmiadmin</code> have incorrect permissions which will prevent app and dataset sharing.. As <code>bmiadmin</code> run the following to set the correct permissions:</div>
<div class="panel-body"><pre><code><%= Mapping.directory_permissions_command %></code></pre></div>
<div class="panel-body">Warning: certain directories owned by <code>bmiadmin</code> have incorrect permissions which will prevent app and dataset sharing. As <code>bmiadmin</code> run the following to set the correct permissions:</div>
<div class="panel-body"><pre><code><%= @directory_permissions_command %></code></pre></div>
</div>
<% end %>
<div class="panel">
Expand Down
4 changes: 2 additions & 2 deletions app/views/mappings/no_apps_installed.html.erb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<div class="container">
<div class="jumbotron">
<h1 class="display-4">No Apps Installed</h1>
<p>Please install one or more apps to <%= ENV['SHARED_APPS_ROOT'] =%>.</p>
<p>Datasets should be installed to <%= ENV['APP_DATASET_ROOT'] =%>.</p>
<p>Please install one or more apps to <%= Configuration.shared_apps_root =%>.</p>
<p>Datasets should be installed to <%= Configuration.app_dataset_root =%>.</p>
</div>
<%= link_to 'Back', mappings_path, class: 'btn btn-default' %>
</div>