Skip to content

Commit

Permalink
4656 document number verification (#4794)
Browse files Browse the repository at this point in the history
* Add migration to define handler to verify document on initiatives_types

* Add management from admin panel of document_number_authorization_handler

* Validate document number on votes if required

* Personal data collection must be enabled for initiative type
* An authorization handler for documents must be defined in the
  initiative type

* Use hash_id to avoid votes with the same author identity

  * Personal data collection must be enabled for initiative type

* Add tests for vote initiative command

* Add changelog
  • Loading branch information
entantoencuanto authored and oriolgual committed Jan 31, 2019
1 parent 7e8429b commit ca09b3b
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -18,6 +18,7 @@
- **decidim-initiatives**: Implement a mechanism to store encrytped personal data of users on votes and decrypt it exporting to PDF [\#4716](https://github.com/decidim/decidim/pull/4716)
- **decidim-initiatives**: Add setting to initiatives types to enable a step to allow initiative signature after passing SMS verification mechanism [\#4792](https://github.com/decidim/decidim/pull/4792)
- **decidim-initiatives**: Allow integration of services to add timestamps and sign PDFs, define example services and use in application generator [\#4805](https://github.com/decidim/decidim/pull/4805)
- **decidim-initiatives**: Add setting to initiatives types to verify document number provided on votes and avoid duplicated votes with the same document [\#4794](https://github.com/decidim/decidim/pull/4794)

**Changed**:

Expand Down
Expand Up @@ -44,7 +44,8 @@ def create_initiative_type
banner_image: form.banner_image,
collect_user_extra_fields: form.collect_user_extra_fields,
extra_fields_legal_information: form.extra_fields_legal_information,
validate_sms_code_on_votes: form.validate_sms_code_on_votes
validate_sms_code_on_votes: form.validate_sms_code_on_votes,
document_number_authorization_handler: form.document_number_authorization_handler
)

return initiative_type unless initiative_type.valid?
Expand Down
Expand Up @@ -45,7 +45,8 @@ def attributes
minimum_committee_members: form.minimum_committee_members,
collect_user_extra_fields: form.collect_user_extra_fields,
extra_fields_legal_information: form.extra_fields_legal_information,
validate_sms_code_on_votes: form.validate_sms_code_on_votes
validate_sms_code_on_votes: form.validate_sms_code_on_votes,
document_number_authorization_handler: form.document_number_authorization_handler
}

result[:banner_image] = form.banner_image unless form.banner_image.nil?
Expand Down
Expand Up @@ -45,7 +45,8 @@ def build_initiative_vote
@vote = @initiative.votes.build(
author: @current_user,
decidim_user_group_id: form.group_id,
encrypted_metadata: form.encrypted_metadata
encrypted_metadata: form.encrypted_metadata,
hash_id: form.hash_id
)
end

Expand Down
Expand Up @@ -17,6 +17,7 @@ class InitiativeTypeForm < Decidim::Form
attribute :collect_user_extra_fields, Boolean
translatable_attribute :extra_fields_legal_information, String
attribute :validate_sms_code_on_votes, Boolean
attribute :document_number_authorization_handler, String

validates :title, :description, translatable_presence: true
validates :online_signature_enabled, inclusion: { in: [true, false] }
Expand Down
50 changes: 50 additions & 0 deletions decidim-initiatives/app/forms/decidim/initiatives/vote_form.rb
Expand Up @@ -13,6 +13,7 @@ class VoteForm < Form
attribute :date_of_birth, Decidim::Attributes::LocalizedDate
attribute :postal_code, String
attribute :encrypted_metadata, String
attribute :hash_id, String

attribute :initiative_id, Integer
attribute :author_id, Integer
Expand All @@ -23,6 +24,9 @@ class VoteForm < Form
validates :initiative_id, presence: true
validates :author_id, presence: true

validate :document_number_authorized, if: :required_personal_data?
validate :document_number_uniqueness, if: :required_personal_data?

def initiative
@initiative ||= Decidim::Initiative.find_by(id: initiative_id)
end
Expand All @@ -38,6 +42,12 @@ def encrypted_metadata
@encrypted_metadata ||= encrypt_metadata
end

def hash_id
Digest::MD5.hexdigest(
"#{initiative_id}-#{document_number || author_id}-#{Rails.application.secrets.secret_key_base}"
)
end

def decrypted_metadata
return unless encrypted_metadata

Expand All @@ -63,6 +73,46 @@ def encrypt_metadata

encryptor.encrypt(metadata)
end

def document_number_authorized
return if initiative.document_number_authorization_handler.blank?

errors.add(:document_number, :invalid) unless authorized? && authorization_handler && authorization.unique_id == authorization_handler.unique_id
end

def document_number_uniqueness
errors.add(:document_number, :taken) if initiative.votes.where(hash_id: hash_id).exists?
end

def author
@author ||= current_organization.users.find_by(id: author_id)
end

def authorization
return unless author && handler_name

@authorization ||= Verifications::Authorizations.new(organization: author.organization, user: author, name: handler_name).first
end

def authorization_status
return unless authorization

Decidim::Verifications::Adapter.from_element(handler_name).authorize(authorization, {}, nil, nil)
end

def authorized?
authorization_status&.first == :ok
end

def handler_name
initiative.document_number_authorization_handler
end

def authorization_handler
return unless document_number && handler_name

@authorization_handler ||= Decidim::AuthorizationHandler.handler_for(handler_name, document_number: document_number)
end
end
end
end
2 changes: 2 additions & 0 deletions decidim-initiatives/app/models/decidim/initiative.rb
Expand Up @@ -175,6 +175,8 @@ def author_avatar_url
# RETURNS string
delegate :banner_image, to: :type

delegate :document_number_authorization_handler, to: :type

def votes_enabled?
published? &&
signature_start_date <= Date.current &&
Expand Down
Expand Up @@ -34,6 +34,16 @@
</div>
<% end %>

<div class="row column">
<%=
form.select(
:document_number_authorization_handler,
current_organization.available_authorizations.map { |name| [t("#{name}.name", scope: "decidim.authorization_handlers"), name] },
include_blank: true
)
%>
</div>

<div class="row">
<div class="columns xlarge-6">
<%= form.upload :banner_image %>
Expand Down
1 change: 1 addition & 0 deletions decidim-initiatives/config/locales/en.yml
Expand Up @@ -28,6 +28,7 @@ en:
banner_image: Banner image
collect_user_extra_fields: Collect user personal data on signature
description: Description
document_number_authorization_handler: Authorization to verify document number on votes
extra_fields_legal_information: Legal information about the collection of
personal data
minimum_committee_members: Minimum of committee members
Expand Down
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddDocumentNumberAuthorizationHandlerToInitiativesTypes < ActiveRecord::Migration[5.2]
def change
add_column :decidim_initiatives_types, :document_number_authorization_handler, :string
end
end
Expand Up @@ -6,14 +6,15 @@ module Decidim
module Initiatives
describe VoteInitiative do
let(:form_klass) { VoteForm }
let(:initiative) { create(:initiative) }
let(:organization) { create(:organization) }
let(:initiative) { create(:initiative, organization: organization) }

let(:current_user) { create(:user, organization: initiative.organization) }
let(:form) do
form_klass
.from_params(
form_params
)
).with_context(current_organization: organization)
end

let(:form_params) do
Expand Down Expand Up @@ -69,7 +70,6 @@ module Initiatives
end

context "when a new milestone is completed" do
let(:organization) { create(:organization) }
let(:initiative) do
create(:initiative,
organization: organization,
Expand Down Expand Up @@ -108,7 +108,6 @@ module Initiatives
end

context "when initiative type requires extra user fields" do
let(:organization) { create(:organization) }
let(:initiative) do
create(
:initiative,
Expand All @@ -117,26 +116,82 @@ module Initiatives
)
end
let(:form_with_personal_data) do
form_klass.from_params(form_params.merge(personal_data_params))
form_klass.from_params(form_params.merge(personal_data_params)).with_context(current_organization: organization)
end

let(:invalid_command) { described_class.new(form, current_user) }
let(:valid_command) { described_class.new(form_with_personal_data, current_user) }
let(:command_with_personal_data) { described_class.new(form_with_personal_data, current_user) }

it "broadcasts invalid when form doesn't contain personal data" do
expect { invalid_command.call }.to broadcast :invalid
end

it "broadcasts ok when form contains personal data" do
expect { valid_command.call }.to broadcast :ok
expect { command_with_personal_data.call }.to broadcast :ok
end

it "stores encrypted user personal data in vote" do
valid_command.call
command_with_personal_data.call
vote = InitiativesVote.last
expect(vote.encrypted_metadata).to be_present
expect(form_klass.from_model(vote).decrypted_metadata).to eq personal_data_params
end

context "when another signature exists with the same hash_id" do
before do
create(:initiative_user_vote, initiative: initiative, hash_id: form_with_personal_data.hash_id)
end

it "broadcasts invalid" do
expect { command_with_personal_data.call }.to broadcast :invalid
end
end

context "when initiative type has document number authorization handler" do
let(:handler_name) { "dummy_authorization_handler" }
let(:unique_id) { "test_digest" }
let!(:authorization_handler) { Decidim::AuthorizationHandler.handler_for(handler_name) }

before do
allow(authorization_handler).to receive(:unique_id).and_return(unique_id)
allow(Decidim::AuthorizationHandler).to receive(:handler_for).and_return(authorization_handler)
initiative.type.update(document_number_authorization_handler: handler_name)
end

context "when current_user doesn't have any authorization for the handler" do
it "broadcasts invalid" do
expect { command_with_personal_data.call }.to broadcast :invalid
end
end

context "when current_user have an an authorization for the handler" do
let!(:authorization) { create(:authorization, granted_at: granted_at, name: handler_name, unique_id: authorization_unique_id, user: current_user) }
let(:authorization_unique_id) { unique_id }
let(:granted_at) { 1.minute.ago }

context "when authorization unique_id is the same as handler unique_id" do
it "broadcasts ok" do
expect { command_with_personal_data.call }.to broadcast :ok
end
end

context "when authorization unique_id is different of handler unique_id" do
let(:authorization_unique_id) { "other" }

it "broadcasts invalid" do
expect { command_with_personal_data.call }.to broadcast :invalid
end
end

context "when authorization is not fully granted" do
let(:granted_at) { nil }

it "broadcasts invalid" do
expect { command_with_personal_data.call }.to broadcast :invalid
end
end
end
end
end
end

Expand Down
Expand Up @@ -21,7 +21,8 @@
minimum_committee_members: 7,
banner_image: Decidim::Dev.test_file("city2.jpeg", "image/jpeg"),
collect_user_extra_fields: true,
extra_fields_legal_information: Decidim::Faker::Localized.sentence(25)
extra_fields_legal_information: Decidim::Faker::Localized.sentence(25),
document_number_authorization_handler: ""
}
end

Expand Down

0 comments on commit ca09b3b

Please sign in to comment.