Commit Graph

5878 Commits

Author SHA1 Message Date
Anamika Aggarwal
86bbfcaa0c Automatically set the redirect URI in OIDC
When we first added OIDC support, we were configuring the redirect URI
in the devise initializer, just like we did for other providers.

Thanks to the changes in the previous commit, that code is no longer in
the devise initializer, which means we can use `url_helpers` to get the
redirect URI.

This means we no longer need to define this URI in the secrets. This is
particularly useful for multitenancy; previously, we had to define the
redirect URI for every tenant because different tenants use different
domains or different subdomains.
2025-10-17 13:46:46 +02:00
Anamika Aggarwal
c3b5232907 Use the same code to configure OIDC for all tenants
We were following the same pattern as we used for other providers like
twitter or facebook, but for OIDC we aren't passing the key and the
secret as separate attributes but only a hash of options. This means we
don't need to duplicate the same logic in the devise initializer and the
`OmniauthTenantSetup` class.

Thanks to these changes, we'll be able to introduce dynamic redirect
URLs for both the default tenant and the other tenants (see next commit).

Note that we could probably apply similar changes for the SAML provider.
We might do so in the future. For other providers, removing the
references to `Rails.application.secrets` broke their configuration when
we tested it back in 2022 as part of the multitenancy feature. We might
check whether that's no longer the case (or whether we made a mistake
during our tests in 2022) in the future.
2025-10-17 13:29:15 +02:00
Sebastia
a73c1184fa Merge pull request #6061 from consuldemocracy/poll_text_answers
Add support for essay poll questions
2025-10-16 15:30:22 +02:00
taitus
f3050a1aa5 Manage correctly results and stats for open-ended questions
Note that we are not including Poll::PartialResults for open-ended
questions resutls. The reason is that we do not contemplate the
possibility of there being open questions in booths. Manually
counting and introducing the votes in the system is not feasible.
2025-10-16 14:26:30 +02:00
taitus
b4b00487cc Add validations for changing votation type 2025-10-16 11:09:34 +02:00
taitus
4e57e311dc Add support for open-ended questions in admin section
Introduce a new "open" votation type for poll questions in the admin
interface. This type allows open answers provided by the user.
2025-10-15 15:52:12 +02:00
Johann
e7f2210380 Add setting to require consent for notifications
Ensure GDPR compliance by default (Article 25 GDPR – privacy by design
and by default). Under GDPR, consent must be freely given, specific,
informed and unambiguous [1]. We were subscribing users without
explicity consent, which goes against the "No pre-ticked boxes"
principle.

For compatibility with existing installations, we're using a setting,
disabled by default. Once we release version 2.4.0 we will enable it by
default, which won't affect existing installations but only new ones.

[1] https://gdprinfo.eu/best-gdpr-newsletter-consent-examples-a-complete-guide-to-compliant-email-marketing
2025-10-09 10:53:00 +02:00
taitus
bc6506da5a Unify Officing and Admin results views
Unify the code from app/views/officing/results/index.html.erb with
app/views/admin/poll/results/_result.html.erb. This prepares the ground
to extract a component in the next commit and avoid duplication.
2025-09-22 14:28:25 +02:00
taitus
896ebc82fd Remove unused go_back_to_new calls and unused error_create key
- Remove two redundant go_back_to_new calls in build_results, since
  @poll.questions.find already raises RecordNotFound if a question
  does not exist.
- Drop the fallback flash translation error_create, which is no longer
  used since commit 592fdffe4e and only remained as a default in
  go_back_to_new.
- Move check_officer_assignment from Officing::BaseController to
  Officing::ResultsController, its only place of use.
2025-09-15 09:49:12 +02:00
Javi Martín
3cf6e9b1ca Merge pull request #6046 from Anamika1608/oidc_auth
Add support for OIDC authentication
2025-09-01 19:55:10 +02:00
Anamika Aggarwal
5e263baed2 Add OIDC section for sign in and sign up page
- name: :oidc → Identifier for this login provider in the app.
- scope: [:openid, :email, :profile] → Tells the provider we want the user’s ID (openid), their email, and basic profile info (name, picture, etc.).
- response_type: :code → Uses Authorization Code Flow, which is more secure because tokens are not exposed in the URL.
- issuer: Rails.application.secrets.oidc_issuer → The base URL of the OIDC provider (e.g., Auth0). Used to find its config.
- discovery: true → Automatically fetches the provider’s endpoints from its discovery document instead of manually setting them.
- client_auth_method: :basic → Sends client ID and secret using HTTP Basic Auth when exchanging the code for tokens.

Add system tests for OIDC Auth

Edit the oauth docs to support OIDC auth
2025-08-29 12:20:16 +02:00
Javi Martín
6da53b5716 Add unique index to poll voters table
Note that Rails 7.1 changes `find_or_create_by!` so it calls
`create_or_find_by!` when no record is found, meaning we'll rarely get
`RecordNotUnique` exceptions when using this method during a race
condition.

Adding this index means we need to remove the uniqueness validation.
According to the `create_or_find_by` documentation [1]:

> Columns with unique database constraints should not have uniqueness
> validations defined, otherwise create will fail due to validation
> errors and find_by will never be called.

We're adding a test that checks what happens when using
`create_or_find_by!`.

Note that we're creating voters combining `create_with` with
`find_or_create_by!`. Using `find_or_create_by!(...)` with all
attributes (including non-key ones like `origin`) fails when a voter
already exists with different values, e.g. an existing `origin: "web"`
and an incoming `"booth"`. In this situation the existing record is not
matched and the unique index raises an exception.

`create_with(...).find_or_create_by!(user: ..., poll: ...)` searches by
the unique key only and applies the extra attributes only on creation.
Existing voters are returned unchanged, which is the intended behavior.

For the `take_votes_from` method, we're handling a (highly unlikely, but
theoretically possible) scenario where a user votes at the same time as
taking voters from another user. For that, we're doing something similar
to what `create_or_find_by!` does: we're updating the `user_id` column
inside a new transaction (using a new transactions avoids a
`PG::InFailedSqlTransaction` exception when there are duplicate
records), and deleting the existing voter when we get a
`RecordNotUnique` exception.

On `Poll::WebVote` we're simply raising an exception when there's
already a user who's voted via booth, because the `Poll::WebVote#update`
method should never be called in this case.

We still need to use `with_lock` in `Poll::WebVote`, but not due to
duplicate voters (`find_or_create_by!` method will now handle the unique
record scenario, even in the case of simultaneous transactions), but
because we use a uniqueness validation in `Poll::Answer`; this
validation would cause an error in simultaneous transactions.

[1] https://api.rubyonrails.org/v7.1/classes/ActiveRecord/Relation.html#method-i-create_or_find_by
2025-08-28 14:42:30 +02:00
Javi Martín
8deb1964bd Show errors when submitting too many answers
This could be the case when JavaScript is disabled.

Note that, in `Poll/WebVote` we're calling `given_answers` inside a
transaction. Putting this code before the transaction resulted in a test
failing sometimes, probably because of a bug that might be possible to
reproduce by doing simultaneous requests.
2025-08-14 13:06:43 +02:00
Javi Martín
7ea4f63b07 Allow blank votes in polls via web
With the old interface, there wasn't a clear way to send a blank ballot.
But now that we've got a form, there's an easy way: clicking on "Vote"
while leaving the form blank.
2025-08-14 13:06:43 +02:00
Javi Martín
a7e1b42b6c Use checkboxes and radio buttons on poll forms
Our original interface to vote in a poll had a few issues:

* Since there was no button to send the form, it wasn't clear that
  selecting an option would automatically store it in the database.
* The interface was almost identical for single-choice questions and
  multiple-choice questions, which made it hard to know which type of
  question we were answering.
* Adding other type of questions, like open answers, was hard since we
  would have to add a different submit button for each answer.

So we're now using radio buttons for single-choice questions and
checkboxes for multiple-choice questions, which are the native controls
designed for these purposes, and a button to send the whole form.

Since we don't have a database table for poll ballots like we have for
budget ballots, we're adding a new `Poll::WebVote` model to manage poll
ballots. We're using WebVote instead of Ballot or Vote because they
could be mistaken with other vote classes.

Note that browsers don't allow removing answers with radio buttons, so
once somebody has voted in a single-choice question, they can't remove
the vote unless they manually edit their HTML. This is the same behavior
we had before commit 7df0e9a96.

As mentioned in c2010f975, we're now adding the `ChangeByZero` rubocop
rule, since we've removed the test that used `and change`.
2025-08-14 13:06:37 +02:00
Sebastia
c7a2389d6b Merge pull request #6022 from consuldemocracy/dependabot/bundler/capistrano3-puma-6.2.0
Bump capistrano3-puma from 6.0.0 to 6.2.0
2025-08-04 16:22:05 +02:00
Anamika Aggarwal
c9bf7797a0 Add multi-tenancy support for SAML 2025-07-23 14:43:45 +02:00
taitus
fcbace069e Only load saml settings when we have related secrets
Without this change the IdpMetaParser would give an error
in the Devise initializer when starting the application.

I found it annoying to have to connect to the VPN so
I decided to add this condition.

Reviewer, feel free to consider this commit unnecessary
and ask to revert it.
2025-07-23 14:43:44 +02:00
Senén Rodero Rodríguez
5cb5f19e4f Use IDPMetadataParse to set up the Idp SAML settings
Co-authored-by: Anamika Aggarwal <anamikaagg18@gmail.com>
2025-07-23 14:43:44 +02:00
Javi Martín
6190d808ba Allow different times between IDP and SP machines
We were having an issue because there was a difference of about 11
seconds between the local times of our machines and the time of the IDP
server. Since right now we can't guarantee the time of these machines is
fully synchronized, for now we're adding a margin of error of one
minute.
2025-07-23 14:43:44 +02:00
taitus
a4709f9da0 Add omniauth saml section for sign in and sign up page
Co-authored-by: Anamika Aggarwal <anamikaagg18@gmail.com>
2025-07-23 14:43:44 +02:00
Sebastia
f4189365ea Merge pull request #5955 from cyrillefr/ReplaceLinkWithButtonInVariousComponentsPartI
Replace link with button in various components part i
2025-07-09 15:26:38 +02:00
cyrillefr
ddfd1bedb3 Replace link with button in devise shared links
Cf. conversation https://github.com/consuldemocracy/consuldemocracy/pull/5955#discussion_r2158715957
Need to get this i18n_spec.rb to run to delete the key
shared.links.signin_with_provider.
2025-07-09 13:48:47 +02:00
taitus
a9ba7425bf Remove redundant puma_bind override
The default "puma_bind" setting was reintroduced in capistrano3-puma 6.2.0
as "unix://.../tmp/sockets/puma.sock". We no longer need to define it
explicitly in deploy.rb.

NOTE: Although the official capistrano3-puma README still states
that "puma_bind" must be explicitly set after upgrading to v6,
this is no longer true since version 6.2.0. The default value
("unix://.../tmp/sockets/puma.sock") has been restored.
2025-07-07 15:06:01 +02:00
taitus
84a88ee1fb Remove overridden puma.service.erb template
We remove our custom puma.service.erb template located at
lib/capistrano/templates, as the upstream capistrano3-puma gem (v6.2.0)
now supports all the adjustments we previously needed.

This template was originally copied and modified in
PR #5842 (Bump capistrano3-puma from 5.2.0 to 6.0.0),
with the following relevant changes:

1. WatchdogSec=0
We disabled systemd's watchdog feature by setting WatchdogSec=0.
This is now covered upstream since v6.1.0 via:
> <% if fetch(:puma_systemd_watchdog_sec) && fetch(:puma_systemd_watchdog_sec) > 0 %>
> WatchdogSec=<%= fetch(:puma_systemd_watchdog_sec) %>
> <% end %>

To preserve the same behavior, we now explicitly set:
> set :puma_systemd_watchdog_sec, 0
in config/deploy.rb, instead of relying on the template override.

2. SyslogIdentifier=puma
In our custom template, we replaced the default line:
> SyslogIdentifier=<%= fetch(:puma_service_unit_name) %>
with:
> SyslogIdentifier=puma

This was done to avoid introducing a new config variable
in the installer. However, now that we want to remove the overridden template,
we must accept the default behavior from the gem:
> set :puma_service_unit_name, -> { "puma_#{fetch(:application)}_#{fetch(:stage)}" }

As a result, the generated SyslogIdentifier will change from "puma" to something
like "puma_consul_staging".

The only implication is that we'll open a PR in the installer
to replace the hardcoded "puma" with the dynamic default,
so everything stays consistent.

Another option would be to override puma_service_unit_name
with "puma" to keep the SyslogIdentifier exactly as before.
However, this would also affect the names of the systemd services and sockets
(e.g., puma.service instead of puma_consul_staging.service),
which may lead to conflicts or require manual cleanup steps like disabling
and removing the old units. For now, we prefer to avoid that.

3. After=network.target
We had removed the "syslog.target" from the "After=" directive,
as we didn't depend on syslog.

However, keeping "After=syslog.target network.target" does not negatively impact us,
since Puma logs to files directly using StandardOutput=append:....
So we can also adopt the default value from template here.

4. ExecStart
In version 6.2.0, a small change was made to the template logic for the ExecStart line.
This change does not affect us. Since we don't define puma_use_login_shell,
the behavior remains exactly the same as before.

With these changes already supported in the gem, we no longer need
a local copy of the puma service template.
2025-07-07 15:05:01 +02:00
dependabot[bot]
123c97771a Bump rubocop from 1.71.2 to 1.75.8
Bumps [rubocop](https://github.com/rubocop/rubocop) from 1.71.2 to 1.75.8.
- [Release notes](https://github.com/rubocop/rubocop/releases)
- [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop/compare/v1.71.2...v1.75.8)

---
updated-dependencies:
- dependency-name: rubocop
  dependency-version: 1.75.8
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Notes:

This commit also includes several style and lint fixes required after
updating RuboCop:

- Removed redundant parentheses now detected by improved
  'Style/RedundantParentheses' (1.72 and 1.75.3).
- Replaced ternary expressions with logical OR when the ternary was
  returning 'true', as flagged by 'Style/RedundantCondition' (1.73).
- Adjusted block variables to resolve new 'Lint/ShadowingOuterLocalVariable'
  offenses (1.75), helping avoid future conflicts during upgrades with
  'rails app:updates'

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-16 16:07:32 +02:00
Sebastia
304055edbd Merge pull request #5994 from consuldemocracy/remove-legacy-ahoy-patch
Remove Ahoy ensure_uuid logic and uuidtools dependency
2025-06-11 16:10:45 +02:00
taitus
f45f5fe98f Remove Ahoy ensure_uuid logic and uuidtools dependency
Ahoy 2.0.0 [1] introduced automatic UUID generation for visit_token and
visitor_token. As a result, the custom ensure_uuid method is no longer
needed and can be safely removed from the initializer.

Since we aren't manually generating UUIDs anymore, we no longer need
the uuidtools dependency.

[1] https://github.com/ankane/ahoy/blob/v2.0.0/README.md#token-generation
2025-06-11 15:45:40 +02:00
taitus
11e84159d9 Remove Spring from the project
IMHO, Spring no longer provides benefits in this project and:

- Spring was already disabled in the test environment since commit e4e0cb5d47
- Rails removed Spring as a default installation option in 2021 [1]

[1] PR #42997 from https://github.com/rails/rails/
2025-06-11 15:07:17 +02:00
taitus
4cd2d2f794 Define Puma log paths for consistency with installer
capistrano3-puma v6.0.0 updated the defaults for puma_access_log and
puma_error_log to use a single file based on puma_env, like:

> set_if_empty :puma_access_log, -> { File.join(shared_path, 'log', "#{fetch(:puma_env)}.log") }
> set_if_empty :puma_error_log, -> { File.join(shared_path, 'log', "#{fetch(:puma_env)}.log") }

However, our installer expect:
- puma_access.log
- puma_error.log

To keep the existing behavior aligned with the installer, we define
the Puma log paths in config/deploy.rb
2025-06-02 15:39:04 +02:00
taitus
19ab1139b9 Define puma_service_unit_name for consistency with installer
In capistrano3-puma v6.0.0, the default for 'puma_service_unit_name' changed to:
>  "#{application}_puma_#{stage}"

But the installer uses the older convention:
> "puma_#{application}_#{stage}"

To ensure consistency and avoid unit name conflicts when switching between
versions or deploying older branches, we now define the variable explicitly
in config/deploy.rb:

> set :puma_service_unit_name, -> { "puma_#{fetch(:application)}_#{fetch(:stage)}" }
2025-06-02 15:39:01 +02:00
taitus
034ecfeacd Copy Puma systemd unit template from capistrano3-puma
This commit copies the default puma.service.erb template from the
capistrano3-puma gem into lib/capistrano/templates. This allows us to
customize the generated systemd unit file during deploy.

Note that we are also removing the `:puma_conf` variable from `config/deploy.rb`,
as the new ExecStart line in the systemd template (based on capistrano3-puma 6.0.0)
does not rely on a separate Puma config file. The command now directly invokes:

  ExecStart=<%= expanded_bundle_command %> exec puma -e <%= fetch(:puma_env) %>

This replaces the older format used in 5.2.0:

  ExecStart=<%= expanded_bundle_command %> exec --keep-file-descriptors puma -C <%= fetch(:puma_conf) %>

which required explicitly setting the Puma config path.
2025-06-02 15:33:36 +02:00
taitus
db361d88f1 Restore puma_bind setting removed in capistrano3-puma 6.0.0
In earlier versions of capistrano3-puma, the puma_bind has a default value to:
unix://.../tmp/sockets/puma.sock via set_if_empty in lib/capistrano/puma.rb.

This default was removed in 6.0.0, requiring to explicitly set :puma_bind in deploy.rb.

This caused the following runtime error during deploy:
> Failed to restart consul_puma_staging.service: Unit
> consul_puma_staging.socket has a bad unit file setting.
2025-05-30 15:56:28 +02:00
taitus
cd61228790 Replace removed Puma systemd tasks by supported equivalents
capistrano3-puma 6.0.0 removed the `puma:systemd:config` and
`puma:systemd:enable` tasks. This commit updates the deploy script to use
the new `puma:install` and `puma:enable` tasks instead.
2025-05-30 15:56:28 +02:00
Javi Martín
5a5c0a716b Use Rails 7.1 defaults and overwrite them
We're still using YAML to serialize the legislation_annotations ranges
column. I'm not sure whether changing the serializer can have
consequences on existing data, and I'm not sure which serializer we
should provide instead. Quoting the Rails configuration guide [1]:

> Unfortunately there isn't really any suitable defaults available in
> Ruby's standard library. JSON could work as a format, but the json
> gems will cast unsupported types to strings which may lead to bugs.

[1] https://guides.rubyonrails.org/v7.1/configuring.html#config-active-record-default-column-serializer
2025-05-20 15:38:52 +02:00
Javi Martín
3432c385bc Use an HTML5 parser in test helpers
I think this doesn't affect us because we use RSpec instead of Rails
test classes. In any case, if it ever affects us, we'll get notified
when a test fails.
2025-05-20 15:38:52 +02:00
Javi Martín
140619ff00 Log exceptions as :error instead of :fatal
As mentioned in the pull request introducing this change [1]:

> FATAL is documented in the Ruby Logger docs as being for "An
> unhandleable error that results in a program crash.", which does not
> really apply to this case since DebugExceptions is handling the error.

So we're using the new default value, which makes more sense.

[1] Pull request 48575 in https://github.com/rails/rails
2025-05-20 15:38:52 +02:00
Javi Martín
cbf11c2514 Use Rails::HTML5::Sanitizer when sanitizing HTML
Since we use a version of Loofah supporting HTML5 since db2d0bb80, the
`Rails::HTML::Sanitizer.best_supported_vendor` method will return the
HTML5 sanitizer. As mentioned in the pull request introducting this
change [1], the libxml2 maintainer wrote:

> it's still a bad idea to use a 20+ years old, unmaintained HTML 4
> parser to sanitize input for the modern web

So we're going with the new default sanitizer.

Note we aren't uncommenting the `action_text.sanitizer_vendor` option
because we don't use Action Text and so it doesn't affect us , and
uncommeting it will raise an error.

Also note we need to change one test because the new sanitizer handles
whitespace slightly differently.

[1] Pull request 48293 in https://github.com/rails/rails
2025-05-20 15:38:52 +02:00
Javi Martín
82c3c6646d Use Rails 7.1 cache format
Just like we mentioned in commit 001eee3d6, according to the Rails
configuration guide [1], with this format, Rails serializes cache
entries more efficiently. Most importantly:

> All formats are backward and forward compatible, meaning cache entries
> written in one format can be read when using another format. This
> behavior makes it easy to migrate between formats without invalidating
> the entire cache.

[1] https://guides.rubyonrails.org/v7.1/configuring.html#config-active-support-cache-format-version
2025-05-20 15:38:52 +02:00
Javi Martín
a6e735dc4e Generate a secure token when initializing a record
This options doesn't affect us because we don't use `has_secure_token`,
so we're just using the new default value.
2025-05-20 15:38:52 +02:00
Javi Martín
e35bf97928 Commit transactions when using return inside them
It doesn't really affect us because we never use `return`, `break` or
`throw` inside transactions, since it would be confusing exactly because
it wouldn't be 100% clear whether the transaction is committed or not.

So we're using the new default value, which will be the only available
option in Rails 7.2 [1].

[1] Commit eccc6061f4 in https://github.com/rails/rails
2025-05-20 15:38:52 +02:00
Javi Martín
d6b642a23d Run after_commit callbacks in order defined
This change doesn't affect us, since we don't use `after_commit`
callbacks, and, among our dependencies, AFAIK only the Devise gem uses
them, and it only defines one after_commit callback when creating a
record and another one when updating it, so we're never going to have
more than one callback being executed after a transaction is finished.
2025-05-20 15:38:52 +02:00
Javi Martín
0d0f3a48ff Use Rails 7.1 marshalling format
Enabling this option is only dangerous when deploying applications using
multiple servers. Since this isn't our case, we can enable it.
2025-05-20 15:38:52 +02:00
Javi Martín
7993f6d1ed Enable before_committed callbacks on all records
This doesn't really affect us because we don't use `before_committed`
callbacks (and neither do any of our dependencies), so we're using the
new default value.
2025-05-20 15:38:52 +02:00
Javi Martín
5ecd65b7dc Precompile filter parameters
This is done for performance reasons. Quoting the pull request
introducing this option [1]:

> A config might be overkill, but I wanted to provide an escape hatch
> for any upgraded apps that might be testing the exact value of the
> action_dispatch.parameter_filter header.

Since we don't test the exact value of action_dispatch.parameter_filter,
we can enable this option.

[1] Pull request 46452 in https://github.com/rails/rails
2025-05-20 15:38:52 +02:00
Javi Martín
529b4d2a98 Don't validate foreign key in required belongs_to
Before this change, every time we saved a record, the association was
validated if we had `belongs_to :something, required: true`. After this
change [1], it's only validated if the `something_id` column is nil (or
`something_type` for polymorphic associations) or if the `something_id`
attribute has changed.

The main difference is that we no longer get validation errors if the
associated record has been deleted. Doesn't affect us much, so we're
going with the new default value.

[1] Pull request 46522 in https://github.com/rails/rails
2025-05-20 15:38:52 +02:00
Javi Martín
364fa2603c Raise exceptions when assigning to attr_readonly attributes
Since we don't use attr_readonly, this option doesn't really affect us.
So we're just using the new default value.
2025-05-20 15:38:52 +02:00
Javi Martín
a1ae4651ff Simplify the way to set the maximum log size
We can use the new configuration option in Rails 7.1, so we don't have
to configure it manually.
2025-05-20 15:38:52 +02:00
Javi Martín
5db773e44f Serialize message data and metadata together
This is the new default option, and its only dangerous when deploying to
applications with multiple servers. Since this isn't our case, we can
enable it.
2025-05-20 15:38:52 +02:00
Javi Martín
e08ba7efbe Use JSON to serialize messages
The new serializer can decrypt legacy messages using the `marshal`
serializer, so there's no risk of losing data when upgrading. Since we
aren't using applications with several servers, where upgrading some
servers might cause issues on the servers that aren't upgraded yet,
we're enabling the option.

[1] See comments in pull request 42846 in https://github.com/rails/rails
2025-05-20 15:38:52 +02:00