Commit 5d559f6

mokha <mokha@cisco.com>
2018-09-22 16:21:23
create user_session model
1 parent 95aec8a
app/controllers/concerns/authentication.rb
@@ -29,10 +29,7 @@ module Authentication
     redirect_to new_mfa_path unless mfa.valid_session?(session[:mfa])
   end
 
-  def apply_current_request_details(uuid: session[:user_id])
-    Current.request_id = request.uuid
-    Current.user_agent = request.user_agent
-    Current.ip_address = request.ip
-    Current.user = User.find_by(uuid: uuid) if uuid.present?
+  def apply_current_request_details
+    Current.access(request, session)
   end
 end
app/models/current.rb
@@ -12,4 +12,12 @@ class Current < ActiveSupport::CurrentAttributes
     super
     self.user = token&.subject
   end
+
+  def access(request, session)
+    self.request_id = request.uuid
+    self.user_agent = request.user_agent
+    self.ip_address = request.ip
+    uuid = session[:user_id]
+    self.user = User.find_by(uuid: uuid) if uuid.present?
+  end
 end
app/models/user_session.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class UserSession < ApplicationRecord
+  belongs_to :user
+  before_validation :set_unique_key
+
+  scope :active, -> { where("accessed_at > ?", 30.minutes.ago).where('created_at > ?', 24.hours.ago).where(revoked_at: nil) }
+
+  def self.authenticate(key)
+    active.find_by(key: key)
+  end
+
+  def revoke!
+    update!(revoked_at: Time.now)
+  end
+
+  def sudo?
+    sudo_enabled_at.present? && sudo_enabled_at > 1.hour.ago
+  end
+
+  def sudo!
+    update!(sudo_enabled_at: Time.now)
+  end
+
+  def access(request)
+    update(
+      accessed_at: Time.now,
+      ip: request.ip,
+      user_agent: request.user_agent,
+    )
+  end
+
+  private
+
+  def set_unique_key
+    self.key = SecureRandom.urlsafe_base64(32)
+  end
+end
app/views/scim/v2/users/show.scim.jbuilder
@@ -20,4 +20,4 @@ json.locale @user.try(:locale)
 json.timezone @user.try(:timezone)
 json.active true
 json.emails [{ value: @user.email, primary: true }]
-json.groups [ ]
+json.groups []
db/migrate/20180922153546_create_user_sessions.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class CreateUserSessions < ActiveRecord::Migration[5.2]
+  def change
+    create_table :user_sessions do |t|
+      t.references :user, foreign_key: true
+      t.string :key
+      t.string :ip
+      t.text :user_agent
+      t.datetime :sudo_enabled_at
+      t.datetime :accessed_at
+      t.datetime :revoked_at
+
+      t.timestamps
+    end
+    add_index :user_sessions, :key, unique: true
+  end
+end
db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2018_09_09_173139) do
+ActiveRecord::Schema.define(version: 2018_09_22_153546) do
 
   create_table "authorizations", force: :cascade do |t|
     t.integer "user_id"
@@ -62,6 +62,20 @@ ActiveRecord::Schema.define(version: 2018_09_09_173139) do
     t.index ["uuid"], name: "index_tokens_on_uuid", unique: true
   end
 
+  create_table "user_sessions", force: :cascade do |t|
+    t.integer "user_id"
+    t.string "key"
+    t.string "ip"
+    t.text "user_agent"
+    t.datetime "sudo_enabled_at"
+    t.datetime "accessed_at"
+    t.datetime "revoked_at"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["key"], name: "index_user_sessions_on_key", unique: true
+    t.index ["user_id"], name: "index_user_sessions_on_user_id"
+  end
+
   create_table "users", force: :cascade do |t|
     t.string "email"
     t.string "uuid", null: false
spec/factories/user_session.rb
@@ -0,0 +1,24 @@
+FactoryBot.define do
+  factory :user_session do
+    user
+    ip { FFaker::Internet.ip_v4_address }
+    user_agent  { "Googlebot/2.1 (+http://www.google.com/bot.html)" }
+    accessed_at { 1.minute.ago }
+
+    trait :idle_timeout_expired do
+      accessed_at { 31.minutes.ago }
+    end
+
+    trait :absolute_timeout_expired do
+      created_at { (24.hours + 1.second).ago }
+    end
+
+    trait :revoked do
+      revoked_at { 1.minute.ago }
+    end
+
+    trait :sudo do
+      sudo_enabled_at { 1.minute.ago }
+    end
+  end
+end
spec/models/user_session_spec.rb
@@ -0,0 +1,53 @@
+require 'rails_helper'
+
+RSpec.describe UserSession do
+  subject { build(:user_session) }
+
+  describe "#revoke!" do
+    before { subject.revoke! }
+
+    specify { expect(subject.revoked_at).to be_present }
+  end
+
+  describe "#access" do
+    let(:request) { double(ip: "192.168.1.1", user_agent: "blah") }
+
+    before { freeze_time }
+    before { subject.access(request) }
+
+    specify { expect(subject.accessed_at).to eql(Time.now) }
+    specify { expect(subject.ip).to eql(request.ip) }
+    specify { expect(subject.user_agent).to eql(request.user_agent) }
+    specify { expect(subject).to be_persisted }
+  end
+
+  describe ".active" do
+    let!(:active_session) { create(:user_session) }
+    let!(:inactive_session) { create(:user_session, :idle_timeout_expired) }
+    let!(:expired_session) { create(:user_session, :absolute_timeout_expired) }
+    let!(:revoked_session) { create(:user_session, :revoked) }
+
+    specify { expect(UserSession.active).to match_array([active_session]) }
+  end
+
+  describe ".authenticate" do
+    let!(:active_session) { create(:user_session) }
+    let!(:inactive_session) { create(:user_session, :idle_timeout_expired) }
+    let!(:expired_session) { create(:user_session, :absolute_timeout_expired) }
+    let!(:revoked_session) { create(:user_session, :revoked) }
+
+    specify { expect(UserSession.authenticate(active_session.key)).to eql(active_session) }
+    specify { expect(UserSession.authenticate("blah")).to be_nil }
+    specify { expect(UserSession.authenticate(inactive_session.key)).to be_nil }
+    specify { expect(UserSession.authenticate(expired_session.key)).to be_nil }
+    specify { expect(UserSession.authenticate(revoked_session.key)).to be_nil }
+  end
+
+  describe ".sudo?" do
+    let!(:sudo_session) { create(:user_session, :sudo) }
+    let!(:non_sudo_session) { create(:user_session) }
+
+    specify { expect(sudo_session).to be_sudo }
+    specify { expect(non_sudo_session).not_to be_sudo }
+  end
+end