From 9266c3ac58404894539e25e514d8d8a6775c701f Mon Sep 17 00:00:00 2001 From: elijah Date: Wed, 11 Mar 2015 01:12:23 -0700 Subject: add support for rotating tokens and sessions databases, and for a special tmp db for test users. --- DEPLOY.md | 5 ++- Gemfile | 4 +- Gemfile.lock | 6 +-- app/models/account.rb | 23 ++++++----- app/models/temporary_user.rb | 78 +++++++++++++++++++++++++++++++++++ app/models/token.rb | 13 +++--- app/models/user.rb | 2 + lib/tasks/leap_web_core_tasks.rake | 35 +++++++++++++++- test/integration/api/srp_test.rb | 10 ++++- test/integration/api/tmp_user_test.rb | 19 +++++++++ test/test_helper.rb | 9 ++++ test/unit/tmp_user_test.rb | 29 +++++++++++++ test/unit/token_test.rb | 2 +- 13 files changed, 208 insertions(+), 27 deletions(-) create mode 100644 app/models/temporary_user.rb create mode 100644 test/integration/api/tmp_user_test.rb create mode 100644 test/unit/tmp_user_test.rb diff --git a/DEPLOY.md b/DEPLOY.md index 23a54a4..05d97cb 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -25,8 +25,9 @@ run `cap deploy` to deploy to the server. Please make sure your deploy includes the following files: -* public/config/provider.json -* config/couchdb.yml +* `public/config/provider.json` -- provider bootstrap file. +* `config/couchdb.yml` -- normal webapp couchdb configuration. +* `config/couchdb.admin.yml` -- configuration used for rake tasks. ## Couch Security ## diff --git a/Gemfile b/Gemfile index 1444ff4..be0ed2f 100644 --- a/Gemfile +++ b/Gemfile @@ -6,8 +6,8 @@ require File.expand_path('../lib/gemfile_tools.rb', __FILE__) gem "rails", "~> 3.2.21" gem "couchrest", "~> 1.1.3" gem "couchrest_model", "~> 2.0.0" -unless ARGV.grep(/assets:precompile/) - gem "couchrest_session_store", "~> 0.2.4" +if ARGV.grep(/assets:precompile/).empty? + gem "couchrest_session_store", "= 0.3.0" end ## AUTHENTICATION diff --git a/Gemfile.lock b/Gemfile.lock index 346155a..4de7e05 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,8 +76,8 @@ GEM couchrest (~> 1.1.3) mime-types (>= 1.15) tzinfo (>= 0.3.22) - couchrest_session_store (0.2.4) - actionpack + couchrest_session_store (0.3.0) + actionpack (~> 3.0) couchrest couchrest_model cucumber (1.3.17) @@ -255,7 +255,7 @@ DEPENDENCIES client_side_validations-simple_form couchrest (~> 1.1.3) couchrest_model (~> 2.0.0) - couchrest_session_store (~> 0.2.4) + couchrest_session_store (= 0.3.0) cucumber-rails debugger factory_girl_rails diff --git a/app/models/account.rb b/app/models/account.rb index e60a356..af470ed 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -20,7 +20,7 @@ class Account user = nil user = User.new(attrs) user.save - if user.persisted? + if !user.tmp? && user.persisted? identity = user.identity identity.user_id = user.id identity.save @@ -52,10 +52,12 @@ class Account def destroy(destroy_identity=false) return unless @user - if destroy_identity == false - Identity.disable_all_for(@user) - else - Identity.destroy_all_for(@user) + if !user.tmp? + if destroy_identity == false + Identity.disable_all_for(@user) + else + Identity.destroy_all_for(@user) + end end @user.destroy end @@ -84,12 +86,11 @@ class Account end def self.creation_problem?(user, identity) - user.nil? || - !user.persisted? || - identity.nil? || - !identity.persisted? || - user.errors.any? || - identity.errors.any? + return true if user.nil? || !user.persisted? || user.errors.any? + if !user.tmp? + return true if identity.nil? || !identity.persisted? || identity.errors.any? + end + return false end # You can hook into the account lifecycle from different engines using diff --git a/app/models/temporary_user.rb b/app/models/temporary_user.rb new file mode 100644 index 0000000..87800ed --- /dev/null +++ b/app/models/temporary_user.rb @@ -0,0 +1,78 @@ +# +# For users with login '*test_user*', we don't want to store these documents in +# the main users db. This is because we create and destroy a lot of test +# users. This weirdness of using a different db for some users breaks a lot of +# things, such as associations. However, this is OK for now since we don't need +# those for running the frequent nagios tests. +# +# This module is included in user.rb. This will only work if it is included +# after designs are defined, otherwise, the design definition will overwrite +# find_by_login(). +# + +module TemporaryUser + extend ActiveSupport::Concern + include CouchRest::Model::DatabaseMethod + + USER_DB = 'users' + TMP_USER_DB = 'tmp_users' + TMP_LOGIN = 'test_user' + + included do + use_database_method :db_name + + # since the original find_by_login is dynamically created with + # instance_eval, it appears that we also need to use instance eval to + # override it. + instance_eval <<-EOS, __FILE__, __LINE__ + 1 + def find_by_login(*args) + if args.grep(/#{TMP_LOGIN}/).any? + by_login.database(tmp_database).key(*args).first() + else + by_login.key(*args).first() + end + end + EOS + end + + module ClassMethods + def get(id, db = database) + super(id, db) || super(id, tmp_database) + end + alias :find :get + + # calls db_name(TMP_LOGIN), then creates a CouchRest::Database + # from the name + def tmp_database + choose_database(TMP_LOGIN) + end + + def db_name(login=nil) + if !login.nil? && login.include?(TMP_LOGIN) + TMP_USER_DB + else + USER_DB + end + end + + # create the tmp db if it doesn't exist. + # requires admin access. + def create_tmp_database! + design_doc.sync!(tmp_database.tap{|db|db.create!}) + end + end + + # + # this gets called each and every time a User object needs to + # access the database. + # + def db_name + self.class.db_name(self.login) + end + + # returns true if this User instance is stored in tmp db. + def tmp? + !login.nil? && login.include?(TMP_LOGIN) + end + +end diff --git a/app/models/token.rb b/app/models/token.rb index ff2ad12..4afd275 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -1,8 +1,15 @@ require 'digest/sha2' class Token < CouchRest::Model::Base + def self.expires_after + APP_CONFIG[:auth] && APP_CONFIG[:auth][:token_expires_after] + end - use_database :tokens + include CouchRest::Model::Rotation + rotate_database :tokens, + :every => 1.month, + :timestamp_field => :last_seen_at, + :timeout => self.expires_after.to_i # in minutes belongs_to :user @@ -23,10 +30,6 @@ class Token < CouchRest::Model::Base self.find Digest::SHA512.hexdigest(token) end - def self.expires_after - APP_CONFIG[:auth] && APP_CONFIG[:auth][:token_expires_after] - end - def self.expired return [] unless expires_after by_last_seen_at.endkey(expires_after.minutes.ago) diff --git a/app/models/user.rb b/app/models/user.rb index 9ac7d3d..52e20dd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -42,6 +42,8 @@ class User < CouchRest::Model::Base view :by_created_at end # end of design + include TemporaryUser # MUST come after designs are defined. + def self.login_starts_with(query) self.by_login.startkey(query).endkey(query + "\ufff0") end diff --git a/lib/tasks/leap_web_core_tasks.rake b/lib/tasks/leap_web_core_tasks.rake index ec6abac..e446f93 100644 --- a/lib/tasks/leap_web_core_tasks.rake +++ b/lib/tasks/leap_web_core_tasks.rake @@ -8,7 +8,6 @@ namespace :couchrest do end namespace :cleanup do - desc "Cleanup all expired session documents" task :sessions => :environment do # make sure this is the same as in @@ -23,3 +22,37 @@ namespace :cleanup do end end +namespace :db do + desc "Rotate the databases, as needed." + task :rotate => :environment do + # + # db rotation must be performed by admin, and since + # CouchRest::Session::Document is not a CouchRest::Model, we need to + # override the default config twice. + # + + CouchRest::Model::Base.configure do |conf| + conf.environment = Rails.env + conf.connection_config_file = File.join(Rails.root, 'config', 'couchdb.admin.yml') + end + Token.rotate_database_now(:window => 1.day) + + CouchRest::Session::Document.configure do |conf| + conf.environment = Rails.env + conf.connection_config_file = File.join(Rails.root, 'config', 'couchdb.admin.yml') + end + CouchRest::Session::Document.rotate_database_now(:window => 1.day) + end + + desc "Delete and recreate temporary databases." + task :deletetmp => :environment do + # db deletion and creation must be performed by admin + CouchRest::Model::Base.configure do |conf| + conf.environment = Rails.env + conf.connection_config_file = File.join(Rails.root, 'config', 'couchdb.admin.yml') + end + User.tmp_database.recreate! + User.design_doc.sync!(User.tmp_database) + end + +end diff --git a/test/integration/api/srp_test.rb b/test/integration/api/srp_test.rb index 946450e..fbef47e 100644 --- a/test/integration/api/srp_test.rb +++ b/test/integration/api/srp_test.rb @@ -32,11 +32,11 @@ class SrpTest < RackTest attr_reader :server_auth - def register_user(login = "integration_test_user", password = 'srp, verify me!') + def register_user(login = "integration_test", password = 'srp, verify me!') cleanup_user(login) post 'http://api.lvh.me:3000/1/users.json', user_params(login: login, password: password) - @user = User.find_by_login(login) + assert(@user = User.find_by_login(login), 'user should have been created: %s' % last_response_errors) @login = login @password = password end @@ -101,4 +101,10 @@ class SrpTest < RackTest SRP::Client.new(params.delete(:login) || @login, params) end end + + def last_response_errors + JSON.parse(last_response.body)['errors'] + rescue + "" + end end diff --git a/test/integration/api/tmp_user_test.rb b/test/integration/api/tmp_user_test.rb new file mode 100644 index 0000000..4c1e659 --- /dev/null +++ b/test/integration/api/tmp_user_test.rb @@ -0,0 +1,19 @@ +require 'test_helper' +require_relative 'srp_test' + +class TmpUserTest < SrpTest + + setup do + register_user('test_user_'+SecureRandom.hex(5)) + end + + test "login with srp" do + authenticate + assert_nil server_auth["errors"] + assert_nil server_auth["error"] + assert_equal ["M2", "id", "token"], server_auth.keys + assert last_response.successful? + assert server_auth["M2"] + end + +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 7959ddb..dfc6627 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -30,3 +30,12 @@ class ActiveSupport::TestCase require 'i18n/missing_translations' at_exit { I18n.missing_translations.dump } end + +# +# Create databases, since the temporary databases might not have been created +# when `rake couchrest:migrate` was run. +# + +Token.create_database! if Token.respond_to?(:create_database) +CouchRest::Session::Document.create_database! if CouchRest::Session::Document.respond_to?(:create_database) +User.create_tmp_database! if User.respond_to?(:create_tmp_database) diff --git a/test/unit/tmp_user_test.rb b/test/unit/tmp_user_test.rb new file mode 100644 index 0000000..55b117f --- /dev/null +++ b/test/unit/tmp_user_test.rb @@ -0,0 +1,29 @@ +require 'test_helper' + +class TmpUserTest < ActiveSupport::TestCase + + test "test_user saved to tmp_users" do + begin + assert User.ancestors.include?(TemporaryUser) + + assert_difference('User.database.info["doc_count"]') do + normal_user = User.create!(:login => 'a'+SecureRandom.hex(5).downcase, + :password_verifier => 'ABCDEF0010101', :password_salt => 'ABCDEF') + refute normal_user.database.to_s.include?('tmp') + end + + assert_difference('User.tmp_database.info["doc_count"]') do + tmp_user = User.create!(:login => 'test_user_'+SecureRandom.hex(5).downcase, + :password_verifier => 'ABCDEF0010101', :password_salt => 'ABCDEF') + assert tmp_user.database.to_s.include?('tmp') + end + ensure + begin + normal_user.destroy + tmp_user.destroy + rescue + end + end + end + +end diff --git a/test/unit/token_test.rb b/test/unit/token_test.rb index b143345..5468650 100644 --- a/test/unit/token_test.rb +++ b/test/unit/token_test.rb @@ -1,6 +1,6 @@ require 'test_helper' -class ClientCertificateTest < ActiveSupport::TestCase +class TokenTest < ActiveSupport::TestCase include StubRecordHelper setup do -- cgit v1.2.3