module ApiTemplateOverride
def allowed_to_render?(fieldset, field, model, options)
+ return false if !super
if options[:select]
- return options[:select].include? field.to_s
+ options[:select].include? field.to_s
+ else
+ true
end
- super
end
end
@object = {
uuid: c.portable_data_hash,
portable_data_hash: c.portable_data_hash,
- manifest_text: c.manifest_text,
+ manifest_text: c.signed_manifest_text,
}
end
else
end
def show
- sign_manifests(@object[:manifest_text])
if @object.is_a? Collection
- render json: @object.as_api_response
+ super
else
render json: @object
end
end
def index
- sign_manifests(*@objects.map { |c| c[:manifest_text] })
super
end
protected
- def apply_filters
+ def load_limit_offset_order_params *args
if action_name == 'index'
# Omit manifest_text from index results unless expressly selected.
@select ||= model_class.api_accessible_attributes(:user).
end
super
end
-
- def sign_manifests(*manifests)
- if current_api_client_authorization
- signing_opts = {
- key: Rails.configuration.blob_signing_key,
- api_token: current_api_client_authorization.api_token,
- ttl: Rails.configuration.blob_signing_ttl,
- }
- manifests.each do |text|
- Collection.munge_manifest_locators(text) do |loc|
- Blob.sign_locator(loc.to_s, signing_opts)
- end
- end
- end
- end
end
t.add :description
t.add :properties
t.add :portable_data_hash
- t.add :manifest_text
+ t.add :signed_manifest_text, as: :manifest_text
+ end
+
+ def self.attributes_required_columns
+ # If we don't list this explicitly, the params[:select] code gets
+ # confused by the way we expose signed_manifest_text as
+ # manifest_text in the API response, and never let clients select
+ # the manifest_text column.
+ super.merge('manifest_text' => ['manifest_text'])
end
def check_signatures
return true if current_user.andand.is_admin
+ # Provided the manifest_text hasn't changed materially since an
+ # earlier validation, it's safe to pass this validation on
+ # subsequent passes without checking any signatures. This is
+ # important because the signatures have probably been stripped off
+ # by the time we get to a second validation pass!
+ return true if @signatures_checked and @signatures_checked == compute_pdh
+
if self.manifest_text_changed?
# Check permissions on the collection manifest.
# If any signature cannot be verified, raise PermissionDeniedError
end
end
end
- true
+ @signatures_checked = compute_pdh
end
def strip_manifest_text
if self.manifest_text_changed?
# Remove any permission signatures from the manifest.
- Collection.munge_manifest_locators(self[:manifest_text]) do |loc|
+ self.class.munge_manifest_locators!(self[:manifest_text]) do |loc|
loc.without_signature.to_s
end
end
end
def set_portable_data_hash
- if (self.portable_data_hash.nil? or (self.portable_data_hash == "") or (manifest_text_changed? and !portable_data_hash_changed?))
- self.portable_data_hash = "#{Digest::MD5.hexdigest(manifest_text)}+#{manifest_text.length}"
+ if (portable_data_hash.nil? or
+ portable_data_hash == "" or
+ (manifest_text_changed? and !portable_data_hash_changed?))
+ @need_pdh_validation = false
+ self.portable_data_hash = compute_pdh
elsif portable_data_hash_changed?
+ @need_pdh_validation = true
begin
loc = Keep::Locator.parse!(self.portable_data_hash)
loc.strip_hints!
if loc.size
self.portable_data_hash = loc.to_s
else
- self.portable_data_hash = "#{loc.hash}+#{self.manifest_text.length}"
+ self.portable_data_hash = "#{loc.hash}+#{portable_manifest_text.bytesize}"
end
rescue ArgumentError => e
errors.add(:portable_data_hash, "#{e}")
end
def ensure_hash_matches_manifest_text
- if manifest_text_changed? or portable_data_hash_changed?
- computed_hash = "#{Digest::MD5.hexdigest(manifest_text)}+#{manifest_text.length}"
- unless computed_hash == portable_data_hash
- logger.debug "(computed) '#{computed_hash}' != '#{portable_data_hash}' (provided)"
- errors.add(:portable_data_hash, "does not match hash of manifest_text")
- return false
- end
+ return true unless manifest_text_changed? or portable_data_hash_changed?
+ # No need verify it if :set_portable_data_hash just computed it!
+ return true if not @need_pdh_validation
+ expect_pdh = compute_pdh
+ if expect_pdh != portable_data_hash
+ errors.add(:portable_data_hash,
+ "does not match computed hash #{expect_pdh}")
+ return false
end
- true
end
def redundancy_status
end
end
- def self.munge_manifest_locators(manifest)
+ def signed_manifest_text
+ if has_attribute? :manifest_text
+ token = current_api_client_authorization.andand.api_token
+ @signed_manifest_text = self.class.sign_manifest manifest_text, token
+ end
+ end
+
+ def self.sign_manifest manifest, token
+ signing_opts = {
+ key: Rails.configuration.blob_signing_key,
+ api_token: token,
+ ttl: Rails.configuration.blob_signing_ttl,
+ }
+ m = manifest.dup
+ munge_manifest_locators!(m) do |loc|
+ Blob.sign_locator(loc.to_s, signing_opts)
+ end
+ return m
+ end
+
+ def self.munge_manifest_locators! manifest
# Given a manifest text and a block, yield each locator,
# and replace it with whatever the block returns.
manifest.andand.gsub!(/ [[:xdigit:]]{32}(\+[[:digit:]]+)?(\+\S+)/) do |word|
def self.for_latest_docker_image(search_term, search_tag=nil, readers=nil)
find_all_for_docker_image(search_term, search_tag, readers).first
end
+
+ protected
+ def portable_manifest_text
+ portable_manifest = self[:manifest_text].dup
+ self.class.munge_manifest_locators!(portable_manifest) do |loc|
+ loc.hash + '+' + loc.size.to_s
+ end
+ portable_manifest
+ end
+
+ def compute_pdh
+ portable_manifest = portable_manifest_text
+ (Digest::MD5.hexdigest(portable_manifest) +
+ '+' +
+ portable_manifest.bytesize.to_s)
+ end
end
class Arvados::V1::CollectionsControllerTest < ActionController::TestCase
- setup do
- # Unless otherwise specified in the test, we want normal/secure behavior.
- permit_unsigned_manifests false
- end
-
- teardown do
- # Reset to secure behavior after each test.
- permit_unsigned_manifests false
- end
-
def permit_unsigned_manifests isok=true
# Set security model for the life of a test.
Rails.configuration.permit_create_collection_with_unsigned_manifest = isok
end
+ def assert_signed_manifest manifest_text, label=''
+ assert_not_nil manifest_text, "#{label} manifest_text was nil"
+ manifest_text.scan(/ [[:xdigit:]]{32}\S*/) do |tok|
+ assert_match(/\+A[[:xdigit:]]+@[[:xdigit:]]{8}\b/, tok,
+ "Locator in #{label} manifest_text was not signed")
+ end
+ end
+
test "should get index" do
authorize_with :active
get :index
"basic Collections index included manifest_text")
end
+ test "collections.get returns signed locators" do
+ permit_unsigned_manifests
+ authorize_with :active
+ get :show, {id: collections(:foo_file).uuid}
+ assert_response :success
+ assert_signed_manifest json_response['manifest_text'], 'foo_file'
+ end
+
test "index with manifest_text selected returns signed locators" do
columns = %w(uuid owner_uuid manifest_text)
authorize_with :active
json_response["items"].each do |coll|
assert_equal(columns, columns & coll.keys,
"Collections index did not respect selected columns")
- loc_regexp = / [[:xdigit:]]{32}\+\d+\S+/
- pos = 0
- while match = loc_regexp.match(coll["manifest_text"], pos)
- assert_match(/\+A[[:xdigit:]]+@[[:xdigit:]]{8}\b/, match.to_s,
- "Locator in manifest_text was not signed")
- pos = match.end(0)
- end
+ assert_signed_manifest coll['manifest_text'], coll['uuid']
end
end
foo_collection = collections(:foo_file)
- # Get foo_file using it's portable data has
+ # Get foo_file using its portable data hash
get :show, {
id: foo_collection[:portable_data_hash]
}
assert_not_nil assigns(:object)
resp = assigns(:object)
assert_equal foo_collection[:portable_data_hash], resp['portable_data_hash']
+ assert_signed_manifest resp['manifest_text']
# The manifest in the response will have had permission hints added.
# Remove any permission hints in the response before comparing it to the source.
"Expected 'duplicate key' error in #{response_errors.first}")
end
- test "create succeeds with duplicate name with ensure_unique_name" do
- permit_unsigned_manifests
- authorize_with :active
- manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
- post :create, {
- collection: {
- owner_uuid: users(:active).uuid,
- manifest_text: manifest_text,
- portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47",
- name: "owned_by_active"
- },
- ensure_unique_name: true
- }
- assert_response :success
- assert_equal 'owned_by_active (2)', json_response['name']
+ [false, true].each do |unsigned|
+ test "create with duplicate name, ensure_unique_name, unsigned=#{unsigned}" do
+ permit_unsigned_manifests unsigned
+ authorize_with :active
+ manifest_text = ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:0:foo.txt\n"
+ if !unsigned
+ manifest_text = Collection.sign_manifest manifest_text, api_token(:active)
+ end
+ post :create, {
+ collection: {
+ owner_uuid: users(:active).uuid,
+ manifest_text: manifest_text,
+ name: "owned_by_active"
+ },
+ ensure_unique_name: true
+ }
+ assert_response :success
+ assert_equal 'owned_by_active (2)', json_response['name']
+ end
end
test "create with owner_uuid set to group i can_manage" do