diff --git a/Gemfile b/Gemfile index a1c774548..8ac724e4d 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,7 @@ gem 'maxmind-db' gem 'openid_connect' gem 'password_strength' gem 'pg' +gem 'pg_search' gem 'pundit' gem 'rails-i18n' gem 'seed-fu' diff --git a/Gemfile.lock b/Gemfile.lock index e2dedf172..600cac540 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -198,6 +198,9 @@ GEM password_strength (1.1.4) activemodel pg (1.5.3) + pg_search (2.3.6) + activerecord (>= 5.2) + activesupport (>= 5.2) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -390,6 +393,7 @@ DEPENDENCIES openid_connect password_strength pg + pg_search pry pry-byebug pry-stack_explorer diff --git a/app/models/encryptable.rb b/app/models/encryptable.rb index c7cfc8f39..cc9f1d981 100644 --- a/app/models/encryptable.rb +++ b/app/models/encryptable.rb @@ -28,6 +28,13 @@ class Encryptable < ApplicationRecord validates :name, presence: true validates :description, length: { maximum: 4000 } + include PgSearch::Model + pg_search_scope :search_by_name, + against: [:name, :description], + using: { + tsearch: { prefix: true } + } + def encrypt(team_password, encryption_algorithm = nil) used_encrypted_attrs.each do |a| encrypt_attr(a, team_password, encryption_algorithm) diff --git a/app/models/folder.rb b/app/models/folder.rb index 6854f9c27..c2ef76091 100644 --- a/app/models/folder.rb +++ b/app/models/folder.rb @@ -25,6 +25,13 @@ class Folder < ApplicationRecord validates :description, length: { maximum: 300 } validates :personal_inbox, uniqueness: { scope: :team, if: :personal_inbox? } + include PgSearch::Model + pg_search_scope :search_by_name, + against: [:name, :description], + using: { + tsearch: { prefix: true } + } + def label name end diff --git a/app/models/team.rb b/app/models/team.rb index bf25c0ea7..3a834802c 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -34,6 +34,13 @@ class Team < ApplicationRecord in_progress: 2 }, _prefix: :recrypt + include PgSearch::Model + pg_search_scope :search_by_name, + against: [:name, :description], + using: { + tsearch: { prefix: true } + } + def label name end diff --git a/app/presenters/teams/filtered_list.rb b/app/presenters/teams/filtered_list.rb index ce555904e..9dafbcb95 100644 --- a/app/presenters/teams/filtered_list.rb +++ b/app/presenters/teams/filtered_list.rb @@ -9,7 +9,7 @@ def fetch_entries else filtered_teams = teams - filtered_teams = filter_by_query(filtered_teams) if query_present? + filtered_teams = SearchStrategy.new.search(query, filtered_teams) if query_present? filtered_teams = filter_by_id(filtered_teams) if team_id.present? end @@ -59,18 +59,6 @@ def limit @params[:limit] end - def filter_by_query(teams) - teams.where( - 'lower(encryptables.description) LIKE :query - OR lower(encryptables.name) LIKE :query - OR lower(folders.name) LIKE :query - OR lower(teams.name) LIKE :query', - query: "%#{query}%" - ) - .references(:folders, - folders: [:encryptables]) - end - def filter_by_id(filtered_teams) filtered_teams.where(id: team_id) end diff --git a/app/presenters/teams/pg_searching.rb b/app/presenters/teams/pg_searching.rb new file mode 100644 index 000000000..40111f955 --- /dev/null +++ b/app/presenters/teams/pg_searching.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module ::Teams + class PgSearching < SearchStrategy + # rubocop:disable Metrics/MethodLength + def search(query, teams) + # Get allowed entities which user is allowed to read + allowed_folders = Folder.where(team_id: teams) + allowed_encryptables = Encryptable.where(folder_id: allowed_folders) + + # Get matching ids with pg_search + matching_team_ids = teams.search_by_name(query).pluck :id + matching_folder_ids = Folder.where({ id: allowed_folders }).search_by_name(query).pluck :id + matching_encryptable_ids = Encryptable.where({ id: allowed_encryptables }) + .search_by_name(query).pluck :id + + # Join allowed tables together and return matching ones + teams.where('encryptables.id IN (:encryptable_ids) OR teams.id IN (:team_ids) OR folders.id +IN (:folder_ids)', + encryptable_ids: matching_encryptable_ids, + team_ids: matching_team_ids, + folder_ids: matching_folder_ids) + .references(:folders, + folders: [:encryptables]) + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/app/presenters/teams/search_strategy.rb b/app/presenters/teams/search_strategy.rb new file mode 100644 index 000000000..cf5f956fe --- /dev/null +++ b/app/presenters/teams/search_strategy.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ::Teams + class SearchStrategy + def search(query, teams) + if database_adapter.include?('postgres') + PgSearching.new.search(query, teams) + else + SqlSearch.new.search(query, teams) + end + end + + def database_adapter + Rails.configuration.database_configuration[Rails.env]['adapter'] + end + end +end diff --git a/app/presenters/teams/sql_search.rb b/app/presenters/teams/sql_search.rb new file mode 100644 index 000000000..ac9d6ed5b --- /dev/null +++ b/app/presenters/teams/sql_search.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ::Teams + class SqlSearch < SearchStrategy + def self.search(query, teams) + teams.where( + 'lower(encryptables.description) LIKE :query + OR lower(encryptables.name) LIKE :query + OR lower(folders.name) LIKE :query + OR lower(teams.name) LIKE :query', + query: "%#{query}%" + ) + .references(:folders, + folders: [:encryptables]) + end + end +end diff --git a/config/application.rb b/config/application.rb index 744034189..6f17947f0 100644 --- a/config/application.rb +++ b/config/application.rb @@ -7,7 +7,6 @@ require_relative 'boot' - # See https://github.com/rails/rails/blob/v6.0.2.1/railties/lib/rails/all.rb for the list # and https://stackoverflow.com/questions/59593542/omit-action-mailbox-activestorage-and-conductor-routes-from-bin-rails-routes-i # of what is being included here @@ -63,7 +62,6 @@ class Application < Rails::Application # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de - # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" @@ -92,7 +90,7 @@ class Application < Rails::Application # config.active_record.schema_format = :sql config.generators do |g| - g.test_framework :minitest, fixture_replacement: :fabrication + g.test_framework :minitest, fixture_replacement: :fabrication g.fixture_replacement :fabrication, dir: "test/fabricators" end diff --git a/config/initializers/postgres-options.rb b/config/initializers/postgres-options.rb new file mode 100644 index 000000000..a3380490e --- /dev/null +++ b/config/initializers/postgres-options.rb @@ -0,0 +1,2 @@ +# We have to discuss this, because we don't want to add it, if we don't use postgres +PgSearch.multisearch_options = { using: { tsearch: {prefix: true} } } \ No newline at end of file diff --git a/db/migrate/20230703095213_create_pg_search_documents.rb b/db/migrate/20230703095213_create_pg_search_documents.rb new file mode 100644 index 000000000..d16e38142 --- /dev/null +++ b/db/migrate/20230703095213_create_pg_search_documents.rb @@ -0,0 +1,17 @@ +class CreatePgSearchDocuments < ActiveRecord::Migration[7.0] + def up + say_with_time("Creating table for pg_search multisearch") do + create_table :pg_search_documents do |t| + t.text :content + t.belongs_to :searchable, polymorphic: true, index: true + t.timestamps null: false + end + end + end + + def down + say_with_time("Dropping table for pg_search multisearch") do + drop_table :pg_search_documents + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 40e3c98f2..93ddb73a6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,10 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_07_19_120958) do +ActiveRecord::Schema[7.0].define(version: 2023_07_03_095213) do + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + create_table "encryptables", force: :cascade do |t| t.string "name", limit: 255, default: "", null: false t.integer "folder_id" @@ -18,7 +21,7 @@ t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.string "type", default: "Account::Credentials", null: false - t.text "encrypted_data", limit: 16777215 + t.text "encrypted_data" t.integer "credential_id" t.text "content_type" t.string "encrypted_transfer_password" @@ -41,7 +44,16 @@ t.index ["name"], name: "index_folders_on_name" end - create_table "settings", force: :cascade do |t| + create_table "pg_search_documents", force: :cascade do |t| + t.text "content" + t.string "searchable_type" + t.bigint "searchable_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["searchable_type", "searchable_id"], name: "index_pg_search_documents_on_searchable" + end + + create_table "settings", id: :serial, force: :cascade do |t| t.string "key", null: false t.string "value" t.string "type", null: false @@ -76,7 +88,7 @@ t.datetime "updated_at", null: false end - create_table "users", force: :cascade do |t| + create_table "users", id: :serial, force: :cascade do |t| t.text "public_key", null: false t.binary "private_key", null: false t.binary "password" @@ -95,7 +107,7 @@ t.integer "human_user_id" t.text "options" t.integer "role", default: 0, null: false - t.integer "default_ccli_user_id" + t.bigint "default_ccli_user_id" t.index ["default_ccli_user_id"], name: "index_users_on_default_ccli_user_id" end