Allow accounts to specify session timeout time

fixes #11388

This should work for single and multiple accounts. 
You can now enable a plugin that lets you set
how long (in minutes) before users on your account
are automatically logged of because of inactivity.
You are required to set this to at least 20 
minutes or more.

Test Plan
  Steps: 
  1. log in as a site admin 
  2. [plugins] 
  3. [Sessions] 
  4. on the account drop down menu, select all 
     accounts, then enter a time in the text field
     in minutes. At least 20 minutes
  5. [Apply] 
  6. log out 
  7. go to /login and make sure the
     "stay signed in" checkbox is checked 
  8. log in with any user that can get on the
     account you enabled the plugin to work for 
  9. wait for a little longer than the amount of
     time you set the plugin for 
  10. try to complete an action, like clicking on
     course or the canvas home page logo

You should be logged out

Thanks Adam for writing this test plan.

Change-Id: If7dc772e4a1a59e646645c698d732308d3e0a19f
Reviewed-on: https://gerrit.instructure.com/15231
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Jeremy Stanley <jeremy@instructure.com>
This commit is contained in:
Sterling Cobb 2012-11-09 14:40:46 -07:00
parent c10ea6f852
commit c0809345a9
12 changed files with 220 additions and 4 deletions

View File

@ -13,7 +13,7 @@ gem 'bcrypt-ruby', '3.0.1'
gem 'builder', '2.1.2'
gem 'daemons', '1.1.0'
gem 'diff-lcs', '1.1.2', :require => 'diff/lcs'
gem 'encrypted_cookie_store-instructure', '1.0.1', :require => 'encrypted_cookie_store'
gem 'encrypted_cookie_store-instructure', '1.0.2', :require => 'encrypted_cookie_store'
gem 'erubis', '2.7.0'
gem 'ffi', '1.1.5'
gem 'hairtrigger', '0.1.14'

View File

@ -17,4 +17,7 @@
#
module PseudonymSessionsHelper
def session_timeout_enabled
PluginSetting.settings_for_plugin 'sessions'
end
end

View File

@ -0,0 +1,26 @@
class SessionsTimeout
def initialize(app)
@app = app
end
# When loading an account, set the expire_after key if they have set up session
# timeouts in the plugin settings. :expire_after is relative to Time.now and
# should be a Fixnum. This will work it's way up to encrypted_cookie_store.rb
# where the session's expire time is determined. EncryptedCookieStore is in a gem.
def call(env)
session_option_key = ActionController::Session::CookieStore::ENV_SESSION_OPTIONS_KEY
sessions_settings = Canvas::Plugin.find('sessions').settings
sessions_timeout = 1.day # defaults to 1 day (in seconds)
# Grab settings, convert them to seconds.(everything is converted down to seconds)
if sessions_settings && sessions_settings["session_timeout"].present?
sessions_timeout = sessions_settings["session_timeout"].to_f.minutes
end
options = env[session_option_key]
options[:expire_after] = sessions_timeout
env[session_option_key] = options
@app.call(env)
end
end

View File

@ -0,0 +1,8 @@
<% fields_for :settings, OpenObject.new(settings) do |f| %>
<table style="width: 500px;" class="formtable">
<tr>
<td><%= f.blabel :session_timeout, :en => "Time before session expires in minutes (20 minimum)" %></td>
<td><%= f.text_field :session_timeout %></td>
</tr>
</table>
<% end %>

View File

@ -102,7 +102,11 @@ $(document).ready(function() {
</div>
<div>
<div class="login-options">
<%= f.check_box :remember_me, :checked => session[:used_remember_me_token] %> <%= f.label :remember_me, :en => "Stay signed in" %>
<% unless session_timeout_enabled %>
<%= f.check_box :remember_me, :checked => session[:used_remember_me_token] %>
<%= f.label :remember_me, :en => "Stay signed in" %>
<% end %>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<% url = (params[:canvas_login] != '1' && @domain_root_account.try(:forgot_password_external_url)) || "#" %><br />
<%= link_to t('dont_know_password', "Don't know your password?"), url, :class => (url != '#' ? "not_external" : "forgot_password_link"), :id => "login_forgot_password" %>

View File

@ -72,6 +72,7 @@ Rails::Initializer.run do |config|
config.autoload_paths += %W( #{RAILS_ROOT}/app/middleware #{RAILS_ROOT}/app/observers )
config.middleware.insert_after(ActionController::Base.session_store, 'SessionsTimeout')
config.middleware.insert_before('ActionController::ParamsParser', 'LoadAccount')
config.middleware.insert_before('ActionController::ParamsParser', 'StatsTiming')
config.middleware.insert_before('ActionController::ParamsParser', 'PreventNonMultipartParse')

View File

@ -190,6 +190,19 @@ if Attachment.s3_storage?
:settings => Attachment.s3_config
})
end
Canvas::Plugin.register('sessions', nil, {
:name => lambda{ t :name, 'Sessions' },
:description => lambda{ t :description, 'Manage session timeouts' },
:website => 'http://www.instructure.com',
:author => 'Instructure',
:author_website => 'http://www.instructure.com',
:version => '1.0.0',
:settings_partial => 'plugins/sessions_timeout',
:validator => 'SessionsValidator',
:settings => nil
})
Canvas::Plugin.register('assignment_freezer', nil, {
:name => lambda{ t :name, 'Assignment Property Freezer' },
:description => lambda{ t :description, 'Freeze Assignment Properties on Copy' },
@ -200,6 +213,7 @@ Canvas::Plugin.register('assignment_freezer', nil, {
:settings_partial => 'plugins/assignment_freezer_settings',
:settings => nil
})
Canvas::Plugin.register('embedly', nil, {
:name => lambda{ t :name, 'Embedly Integration' },
:description => lambda{ t :description, 'Pull Embedly info for Collections' },

View File

@ -0,0 +1,28 @@
#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas 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 Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module Canvas::Plugins::Validators::SessionsValidator
def self.validate(settings, plugin_setting)
timeout = settings["session_timeout"].to_f.minutes
if timeout < 20.minutes
plugin_setting.errors.add_to_base(I18n.t('canvas.plugins.errors.login_expiration_minimum', 'Session expiration must be 20 minutes or greater'))
else
settings
end
end
end

View File

@ -26,4 +26,25 @@ describe PseudonymSessionsHelper do
included_modules.should be_include(PseudonymSessionsHelper)
end
describe "#session_timeout_enabled" do
context "when the sessions plugin is enabled" do
before do
PluginSetting.expects(:settings_for_plugin).with('sessions').returns({"session_timeout" => 123})
end
it "returns true" do
helper.session_timeout_enabled.should be_true
end
end
context "when the sessions plugin is disabled" do
before do
PluginSetting.expects(:settings_for_plugin).with('sessions').returns(nil)
end
it "returns false" do
helper.session_timeout_enabled.should be_false
end
end
end
end

View File

@ -19,11 +19,14 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe CollectionItemsController do
before(:all) { Bundler.require :embedly }
before(:all) {Bundler.require :embedly}
context "#link_data" do
before do
PluginSetting.expects(:settings_for_plugin)
end
it "should error if the user isn't logged in" do
PluginSetting.expects(:settings_for_plugin).never
post "/collection_items/link_data", :url => "http://www.example.com/"
response.status.to_i.should == 401
end
@ -45,14 +48,18 @@ describe CollectionItemsController do
it "should return basic data for free embedly accounts" do
user_session(user)
data = OpenObject.new(:title => "t1", :description => "d1", :thumbnail_url => "/1.jpg")
PluginSetting.expects(:settings_for_plugin).with(:embedly).returns({ :api_key => 'test', :plan_type => 'free'})
Embedly::API.any_instance.expects(:oembed).with(
:url => "http://www.example.com/",
:autoplay => true,
:maxwidth => Canvas::Embedly::MAXWIDTH
).returns([data])
post "/collection_items/link_data", :url => "http://www.example.com/"
response.should be_success
json_parse.should == {
'title' => data.title,
'description' => data.description,
@ -65,6 +72,7 @@ describe CollectionItemsController do
it "should return extended data for paid embedly accounts" do
user_session(user)
data = OpenObject.new(:title => "t1", :description => "d1", :images => [{'url' => 'u1'},{'url' => 'u2'}], :object => OpenObject.new(:html => "<iframe src='test'></iframe>"))
PluginSetting.expects(:settings_for_plugin).with(:embedly).returns({ :api_key => 'test', :plan_type => 'paid'})
Embedly::API.any_instance.expects(:preview).with(
:url => "http://www.example.com/",

View File

@ -0,0 +1,58 @@
#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas 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 Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe "Session Timeout" do
context " when sessions timeout is set to 30 minutes" do
before do
plugin_setting = PluginSetting.new(:name => "sessions", :settings => {"session_timeout" => "30"})
plugin_setting.save!
end
context "when a user logs in" do
before do
course_with_student(:active_all => true, :user => user_with_pseudonym)
login_as
end
it "should time out after 40 minutes of inactivity" do
now = Time.now
get "/"
response.should be_success
Time.stubs(:now).returns(now + 40.minutes)
get "/"
response.should redirect_to "http://www.example.com/login"
end
it "should not time out if the user remains active" do
now = Time.now
get "/"
response.should be_success
Time.stubs(:now).returns(now + 20.minutes)
get "/"
response.should be_success
Time.stubs(:now).returns(now + 40.minutes)
get "/"
response.should be_success
end
end
end
end

View File

@ -0,0 +1,45 @@
require File.expand_path('spec/selenium/common')
describe "Sessions Timeout" do
it_should_behave_like "in-process server selenium tests"
describe "Validations" do
context "when you are logged in as an admin" do
before do
user_logged_in
Account.site_admin.add_user(@user)
end
it "requires session expiration to be at least 20 minutes" do
get "/plugins/sessions"
f("#plugin_setting_disabled").click
f('#settings_session_timeout').send_keys('19')
expect_new_page_load{ f('.save_button').click }
assert_flash_error_message /There was an error saving the plugin settings/
end
end
end
context "when sessions timeout is set to .04 minutes" do
before do
plugin_setting = PluginSetting.new(:name => "sessions", :settings => {"session_timeout" => ".04"})
plugin_setting.save!
end
context "when a user is logged in" do
before do
user_with_pseudonym({:active_user => true})
login_as
f('.user_name').text.should == @user.primary_pseudonym.unique_id
end
it "logs the user out after 3 seconds" do
sleep 3
get "/courses"
assert_flash_warning_message /You must be logged in to access this page/
end
end
end
end