Some institutions using CONSUL have expressed interest in this feature
since some of their tenants might already have their own domains.
We've considered many options for the user interface to select whether
we're using a subdomain or a domain, like having two separate fields,
using a check box, ... In the end we've chosen radio buttons because
they make it easier to follow a logical sequence: first you decide
whether you're introducing a domain or subdomain, and then you enter it.
We've also considered hiding this option and assuming "if it's got a
dot, it's a domain". However, this wouldn't work with nested subdomains
and it wouldn't work with domains which are simply machine names.
Note that a group of radio buttons (or check boxes) is difficult to
style when the text of the label might expand over more than one line
(as is the case here on small screens); in this case, most solutions
result in the second line of the label appearing immediately under the
radio button, instead of being aligned with the first line of the label.
That's why I've added a container for the input+label combination.
Just like we respond with "not found" for any other record. This
improves the user experience because with the "Not found" error message
people realize the URL is wrong instead of thinking that they broke the
application.
We were displaying an icon showing that certain actions can't be
performed. However, people who can't see the icons were hearing that
they _can_ perform certain actions while the opposite is true.
We've considered other options to solve this problem. One was to split
the list in two: actions that can be performed and actions that can't be
performed. It was tricky because in some cases we're listing that
actions that can be performed now and in other cases we're displaying
the actions that people will be able to perform once they verify their
account.
Another option was to include the word "Cannot" as a prefix instead of
"Additional verification needed". We haven't done so because, while in
English we say "cannot do this thing", in other languages they say
"this thing cannot do".
So we've gone with a solution where people hearing what's on the screen
know what's going on and we don't have to make big changes in the code.
This is something we had read about a long time ago, but didn't find how
to reproduce the issue until now.
As mentioned in the Apartment documentation:
> it's important to consider that you may want to maintain the
> "selected" tenant through different parts of the Rack application
> stack. For example, the Devise gem adds the Warden::Manager middleware
> at the end of the stack in the examples above, our
> Apartment::Elevators::Subdomain middleware would come after it.
> Trouble is, Apartment resets the selected tenant after the request is
> finished, so some redirects (e.g. authentication) in Devise will be
> run in the context of the "public" tenant. The same issue would also
> effect a gem such as the better_errors gem which inserts a middleware
> quite early in the Rails middleware stack.
>
> To resolve this issue, consider adding the Apartment middleware at a
> location in the Rack stack that makes sense for your needs, e.g.:
>
> Rails.application.config.middleware.insert_before Warden::Manager,
> Apartment::Elevators::Subdomain
>
> Now work done in the Warden middleware is wrapped in the
> Apartment::Tenant.switch context started in the Generic elevator.
We were using the same logic in many different places, so we're
simplifying the code. I'm not convinced about the method names, though,
so we might change them in the future.
Note using this method for the default tenant in the `TenantDiskService`
class resulted in a `//` in the path, which is probably harmless but
very ugly and it also generates a different key than the one we got
until now. I've added an extra test to make sure that isn't the case.
On my machine, seeding a tenant takes about one second, so skipping this
action when it isn't necessary makes tests creating tenants faster
(although creating a tenant still takes about 3-4 seconds on my
machine).
Note we don't need to update the tests; the tests themselves help us
confirm that `Rails.application.secrets` and `Tenant.current_secrets`
return the same object on single-tenant applications.
Right now this is configured using the `secrets.yml` file, which is the
file we've used in the past to configure SMTP settings.
Note that, in the `current_secrets` method, the `if default?` condition
is there so in single-tenant applications it returns the exact same
object as `Rails.application.secrets`, and it makes it immediately clear
for developers reading the code. We're also caching the tenant secrets
(using `||=`) so they behave the same way as Rails secrets; for this to
work properly 100% of the time (for example, in tests) we need to expire
these cached secrets whenever the Rails secrets change.
A similar `unless Tenant.default?` condition is present in the
ApplicationMailer because there's a chance some CONSUL installations
might not be using secrets to define the SMTP settings(they might be
using environment variables, for example) and so in this case we don't
want to force settings based on the secrets.yml file because it would
break the application.
The structure of the SMTP settings in the secrets file should be:
```
production:
tenants:
name_of_the_tenant_subdomain:
smtp_settings:
address:
(...)
```
While this is not strictly necessary, it can help moving the data of one
tenant to a different server or removing it.
Note we're using subfolders inside the `tenants` subfolder. If we simply
used subfolders with the schema names, if the schema names were, for
instance, language codes like `es`, `en`, `it`, ... they would conflict
with the default subfolders used by Active Storage.
Note we aren't allowing to delete a tenant because it would delete all
its data, so this action is a very dangerous one. We might need to add a
warning when creating a tenant, indicating the tenant cannot be
destroyed. We can also add an action to delete a tenant which forces the
admin to write the name of the tenant before deleting it and with a big
warning about the danger of this operation.
For now, we're letting administrators of the "main" (default) tenant to
create other tenants. However, we're only allowing to manage tenants
when the multitenancy configuration option is enabled. This way the
interface won't get in the way on single-tenant applications.
We've thought about creating a new role to manage tenants or a new URL
out of the admin area. We aren't doing so for simplicity purposes and
because we want to keep CONSUL working the same way it has for
single-tenant installations, but we might change it in the future.
There's also the fact that by default we create one user with a known
password, and if by default we create a new role and a new user to
handle tenants, the chances of people forgetting to change the password
of one of these users increases dramatically, particularly if they
aren't using multitenancy.
While we ping some search engines (currently, only Google) when
generating the sitemap files, we weren't telling search engines
accessing through the `robots.txt` file where to find the sitemap. Now
we're doing so, using the right sitemap file for the right tenant.
Note that the `sitemap:refresh` task only pings search engines at the
end, so it only does so for the `Sitemap.default_host` defined last. So
we're using the `sitemap:refresh:no_ping` task instead and pinging
search engines after creating each sitemap.
Note we're pinging search engines in staging and preproduction
environments. I'm leaving it that way because that's what we've done
until now, but I wonder whether we should only do so on production.
Since we're creating a new method to get the current url_options, we're
also using it in the dev_seeds.
Since records in different tenants can have the same ID, they can share
the same `cache_key`, and so we need a namespace to differentiate them.
Without them, records from one tenant could expire the cache of a record
from another tenant.
The subdomain elevator we were using, which is included in apartment,
didn't work on hosts already including a subdomain (like
demo.consul.dev, for instance). In those cases, we would manually add
the subdomain to the list of excluded subdomains. Since these subdomains
will be different for different CONSUL installations, it meant each
installation had to customize the code. Furthermore, existing
installations using subdomains would stop working.
So we're using a custom method to find the current tenant, based on the
host defined in `default_url_options`.
In order to avoid any side-effects on single-tenant applications, we're
adding a new configuration option to enable multitenancy
We're enabling two ways to handle this configuration option:
a) Change the application_custom.rb file, which is under version control
b) Change the secrets.yml file, which is not under version control
This way people prefering to handle configuration options through
version control can do so, while people who prefer handling
configuration options through te secrets.yml file can do so as well.
We're also disabling the super-annoying warnings mentioning there are no
tenants which we got every time we run migrations on single-tenant
applications. These messages will only be enabled when the multitenancy
feature is enabled too. For this reason, we're also disabling the
multitenancy feature in the development environment by default.
This way all tenants will be able to access them instead of just the
default one.
The apartment gem recommends using a rake task instead of a migration,
but that's a solution which is primarily meant for new installations.
Migrations are easier to execute on existing installations.
However, since this migration doesn't affect the `schema.rb` file, we
still need to make sure the shared schema is created in tasks which do
not execute migrations, like `db:schema:load` or `db:test:prepare`, just
like the apartment gem recommends. That's why we're enhancing these
tasks so they execute this migration.
Note that there might be cases where the database user isn't a superuser
(as it's usually the case on production environments), meaning commands
to create, alter or drop extensions will fail. There's also the case
where users don't have permissions to create schemas, which is needed in
order to create the shared extensions schema and the schemas used by the
tenants. For these reasons, we're minimizing the number of commands, and
so we only alter or create extensions when it is really necessary.
When users don't have permission, we aren't running the commands but
showing a warning with the steps needed to run the migration manually.
This is only necessary on installations which are going to use
multitenancy; single-tenant applications upgrading don't need to run
this migration, and that's why we aren't raising exceptions when we
can't run it.
For new installations, we'll change the CONSUL installer so extensions
are automatically created in the shared schema.
Also note the plpgsql extension is not handled here. This is a special
extension which must be installed on the pg_catalog schema, which is
always in the search path and so is shared by all tenants.
Finally, we also need to change the `database.yml` file in order to
search for shared extensions while running migrations or model tests,
since none of our enabled extensions are executed during migrations;
we're also adding a rake task for existing installations. Quoting the
apartment documentation:
> your database.yml file must mimic what you've set for your default and
> persistent schemas in Apartment. When you run migrations with Rails,
> it won't know about the extensions schema because Apartment isn't
> injected into the default connection, it's done on a per-request
> basis.
Rails 6.0 introduced a `hosts` option which, in the development
environment, defaults to all IP addresses and the `localhost` domain.
However, we can't work with subdomains using `localhost`. For that
purpose, the `lvh.me` domain was created [1].
So we're allowing this domain and its subdomains so we can use them
while working with multitenancy in the development environment.
[1] http://railscasts.com/episodes/123-subdomains-revised
The `reload` method added to max_votes validation is needed because the
author gets here with some changes because of the around_action
`switch_locale`, which adds some changes to the current user record and
therefore, the lock method raises an exception when trying to lock it
requiring us to save or discard those record changes.
We were duplicating the asset host and the URL host in all environments,
but we can make it so the asset host uses the URL host unless we
specifically set it.
Note that, inside the `ApplicationMailer`, the `root_url` method already
uses `default_url_options` to generate the URL.
In the rare case of CONSUL installations who have changed the asset
host, this change has no effect since they'll get a conflict in the
environment files when upgrading and they'll choose to use their own
asset host.
Until now, when editing a specific environment, other CONSUL
installations had to edit the original file, which made it harder to
upgrade.
Now it's possible to change the default CONSUL settings using custom
files, making it easier to upgrade to versions of CONSUL which change
the original environment files (which is very common when upgrading
versions of Rails).
Before this change log generated by OmniAuth was sent to the STDOUT. Now
developers will find those gems log within the `app/log` directory as expected.
In the dev seeds, we were using `Setting["url"]/proposals`, but we can
use `proposals_url` instead, similar to what we do in the rest of the
application.
We can do a similar thing in the sitemap. This way the sitemap will also
work on installations who haven't manually set the "url" setting.
Since we aren't using `Setting["url"]` anywhere anymore, we're removing
it.
This setting was mainly redundant, since we already had the
`server_name` in the secrets. Furthermore, `server_name` is
automatically configured when running the installer, while
`Setting["url"]` had to be manually set in the admin section the
application was installed.
Note we're using `ActionMailer::Base` setting to generate URLs. Sounds a
bit strange, but it's a standard way Rails provides to generate URLs
outside the context of a request.
This task was "temporarily" removed in commit 7b6619528. Since that was
done three and a half years ago, right after the dashboard was
introduced, I think it's time to make this "temporary" measure a bit
more permanent ;).
Note that the `create` action doesn't create an image but updates an
answer instead. We're removing the references to `:create` in the
abilities since it isn't used.
In the future we might change the form to add an image to an answer
because it's been broken for ages since it shows all the attached
images.
Adding, modifiying, and/or deleting questions for an already started
poll is far away from being democratic and can lead to unwanted side
effects like missing votes in the results or stats.
So, from now on, only modifiying questions will be possible only if
the poll has not started yet.
We need to update a couple of tests because a poll is created in the
tests with a timestamp that includes nanoseconds and in the form to edit
the time of the poll the nanoseconds are not sent, meaning it was
detected as a change.
These routes aren't used since commits (2993ef8707, 88a7a29d27)
In parallel while these routes were being removed, the route file
was being split into different files, and they were included again
in the commit 1cd47da9d4.
Until now, in order to edit an answer, we had to click on its title on
the table and then on the "Edit answer" link.
That was tedious and different from what we usually do in the admin
section. Furthermore, the code for the answers table was written twice
and when we modified it we forgot to update the one in the `show`
action, meaning the table here provided less information than the
information present in the answers tables.
Co-Authored-By: Javi Martín <javim@elretirao.net>