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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

4656 document number verification #4794

Merged
merged 6 commits into from Jan 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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