User SqlBypass sessions to avoid ActiveRecord for session storage (#9142)
* Use SqlBypass sessions to avoid ActiveRecord for session storage * Set default host if Capybara.app_host does not existpull/9146/head
parent
79832c27ea
commit
0d71772c37
@ -0,0 +1,55 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
## |
||||
# An AR helper class to access sessions, but not create them. |
||||
# You can still use AR methods to delete records however. |
||||
module Sessions |
||||
class ActiveRecord < ::ApplicationRecord |
||||
self.table_name = 'sessions' |
||||
|
||||
scope :for_user, ->(user) do |
||||
user_id = user.is_a?(User) ? user.id : user.to_i |
||||
|
||||
where(user_id: user_id) |
||||
end |
||||
|
||||
scope :non_user, -> do |
||||
where(user_id: nil) |
||||
end |
||||
|
||||
## |
||||
# Mark all records as readonly so they cannot |
||||
# modify the database |
||||
def readonly? |
||||
true |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,118 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
## |
||||
# An extension to the SqlBypass class to store |
||||
# sessions in database without going through ActiveRecord |
||||
module Sessions |
||||
class SqlBypass < ::ActiveRecord::SessionStore::SqlBypass |
||||
class << self |
||||
## |
||||
# Looks up session data for a given session ID. |
||||
# |
||||
# This is not specific to AR sessions which are stored as AR records. |
||||
# But this is the probably the first place one would search for session-related |
||||
# methods. I.e. this works just as well for cache- and file-based sessions. |
||||
# |
||||
# @param session_id [String] The session ID as found in the `_open_project_session` cookie |
||||
# @return [Hash] The saved session data (user_id, updated_at, etc.) or nil if no session was found. |
||||
def lookup_data(session_id) |
||||
if Rails.application.config.session_store == ActionDispatch::Session::ActiveRecordStore |
||||
find_by_session_id(session_id)&.data |
||||
else |
||||
session_store = Rails.application.config.session_store.new nil, {} |
||||
_id, data = session_store.instance_eval do |
||||
find_session({}, Rack::Session::SessionId.new(session_id)) |
||||
end |
||||
|
||||
data.presence |
||||
end |
||||
end |
||||
end |
||||
|
||||
## |
||||
# Save while updating the user_id reference and updated_at column |
||||
def save |
||||
return false unless loaded? |
||||
|
||||
if @new_record |
||||
insert! |
||||
else |
||||
update! |
||||
end |
||||
end |
||||
|
||||
## |
||||
# Also destroy any other session when this one is actively destroyed |
||||
def destroy |
||||
delete_user_sessions |
||||
super |
||||
end |
||||
|
||||
private |
||||
|
||||
def user_id |
||||
id = data.with_indifferent_access['user_id'].to_i |
||||
id > 0 ? id : nil |
||||
end |
||||
|
||||
def insert! |
||||
@new_record = false |
||||
connection.update <<~SQL, 'Create session' |
||||
INSERT INTO sessions (session_id, data, user_id, updated_at) |
||||
VALUES ( |
||||
#{connection.quote(session_id)}, |
||||
#{connection.quote(self.class.serialize(data))}, |
||||
#{connection.quote(user_id)}, |
||||
(now() at time zone 'utc') |
||||
) |
||||
SQL |
||||
end |
||||
|
||||
def update! |
||||
connection.update <<~SQL, 'Update session' |
||||
UPDATE sessions |
||||
SET |
||||
data=#{connection.quote(self.class.serialize(data))}, |
||||
session_id=#{connection.quote(session_id)}, |
||||
user_id=#{connection.quote(user_id)}, |
||||
updated_at=(now() at time zone 'utc') |
||||
WHERE session_id=#{connection.quote(@retrieved_by)} |
||||
SQL |
||||
end |
||||
|
||||
def delete_user_sessions |
||||
uid = user_id |
||||
return unless uid && OpenProject::Configuration.drop_old_sessions_on_logout? |
||||
|
||||
::Sessions::ActiveRecord.for_user(uid).delete_all |
||||
end |
||||
end |
||||
end |
@ -1,71 +0,0 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
require "active_support/core_ext/module/attribute_accessors" |
||||
|
||||
class UserSession < ActiveRecord::SessionStore::Session |
||||
belongs_to :user |
||||
|
||||
## |
||||
# Keep an index on the current user for the given session hash |
||||
before_save :set_user_id |
||||
|
||||
## |
||||
# Delete related sessions when an active session is destroyed |
||||
after_destroy :delete_user_sessions |
||||
|
||||
## |
||||
# Looks up session data for a given session ID. |
||||
# |
||||
# This is not specific to AR sessions which are stored as `UserSession` records. |
||||
# But this is the probably the first place one would search for session-related |
||||
# methods. I.e. this works just as well for cache- and file-based sessions. |
||||
# |
||||
# @param session_id [String] The session ID as found in the `_open_project_session` cookie |
||||
# @return [Hash] The saved session data (user_id, updated_at, etc.) or nil if no session was found. |
||||
def self.lookup_data(session_id) |
||||
session_store = Rails.application.config.session_store.new nil, {} |
||||
_id, data = session_store.find_session({}, Rack::Session::SessionId.new(session_id)) |
||||
|
||||
data if data.present? |
||||
end |
||||
|
||||
private |
||||
|
||||
def set_user_id |
||||
write_attribute(:user_id, data['user_id']) |
||||
end |
||||
|
||||
def delete_user_sessions |
||||
user_id = data['user_id'] |
||||
return unless user_id && OpenProject::Configuration.drop_old_sessions_on_logout? |
||||
|
||||
::UserSession.where(user_id: user_id).delete_all |
||||
end |
||||
end |
@ -0,0 +1,14 @@ |
||||
class MigrateSessionsUnlogged < ActiveRecord::Migration[6.1] |
||||
def change |
||||
truncate :sessions |
||||
|
||||
# Set the table to unlogged |
||||
execute <<~SQL |
||||
ALTER TABLE "sessions" SET UNLOGGED |
||||
SQL |
||||
|
||||
# We don't need the created at column |
||||
# that now no longer is set by rails |
||||
remove_column :sessions, :created_at, null: false |
||||
end |
||||
end |
@ -0,0 +1,89 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe ::Sessions::ActiveRecord do |
||||
subject { described_class.new session_id: 'foo' } |
||||
|
||||
describe '#save' do |
||||
it 'can not save' do |
||||
expect { subject.save }.to raise_error(ActiveRecord::ReadOnlyRecord) |
||||
expect { subject.save! }.to raise_error(ActiveRecord::ReadOnlyRecord) |
||||
end |
||||
end |
||||
|
||||
describe '#update' do |
||||
let(:session) { FactoryBot.create :user_session } |
||||
subject { described_class.find_by(session_id: session.session_id) } |
||||
|
||||
it 'can not update' do |
||||
expect { subject.save }.to raise_error(ActiveRecord::ReadOnlyRecord) |
||||
expect { subject.save! }.to raise_error(ActiveRecord::ReadOnlyRecord) |
||||
|
||||
expect { subject.update(session_id: 'foo') }.to raise_error(ActiveRecord::ReadOnlyRecord) |
||||
expect { subject.update!(session_id: 'foo') }.to raise_error(ActiveRecord::ReadOnlyRecord) |
||||
end |
||||
end |
||||
|
||||
describe '#destroy' do |
||||
let(:sessions) { FactoryBot.create :user_session } |
||||
|
||||
it 'can not destroy' do |
||||
expect { subject.destroy }.to raise_error(ActiveRecord::ReadOnlyRecord) |
||||
expect { subject.destroy! }.to raise_error(ActiveRecord::ReadOnlyRecord) |
||||
end |
||||
end |
||||
|
||||
describe '.for_user' do |
||||
let(:user) { FactoryBot.create :user } |
||||
let!(:sessions) { FactoryBot.create_list :user_session, 2, user: user } |
||||
|
||||
subject { described_class.for_user(user) } |
||||
|
||||
it 'can find and delete, but not destroy those sessions' do |
||||
expect(subject.pluck(:session_id)).to match_array(sessions.map(&:session_id)) |
||||
|
||||
expect { subject.destroy_all }.to raise_error(ActiveRecord::ReadOnlyRecord) |
||||
|
||||
expect { subject.delete_all }.not_to raise_error |
||||
|
||||
expect(described_class.for_user(user).count).to eq 0 |
||||
end |
||||
end |
||||
|
||||
describe '.non_user' do |
||||
let!(:session) { FactoryBot.create :user_session, user: nil } |
||||
|
||||
subject { described_class.non_user } |
||||
|
||||
it 'can find those sessions' do |
||||
expect(subject.pluck(:session_id)).to contain_exactly(session.session_id) |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue