Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .semaphore/deploy_production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Deploy to production
agent:
machine:
type: e1-standard-2
os_image: ubuntu2004
os_image: ubuntu2404
blocks:
- name: Docker
task:
Expand Down
2 changes: 1 addition & 1 deletion .semaphore/deploy_stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Deploy to stable
agent:
machine:
type: e1-standard-2
os_image: ubuntu2004
os_image: ubuntu2404
execution_time_limit:
hours: 10
blocks:
Expand Down
2 changes: 1 addition & 1 deletion .semaphore/deploy_staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Deploy to staging
agent:
machine:
type: e1-standard-2
os_image: ubuntu2004
os_image: ubuntu2404
execution_time_limit:
hours: 10
blocks:
Expand Down
4 changes: 3 additions & 1 deletion .semaphore/semaphore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Test ETEngine
agent:
machine:
type: e1-standard-2
os_image: ubuntu2004
os_image: ubuntu2404
execution_time_limit:
hours: 6
blocks:
Expand All @@ -19,6 +19,8 @@ blocks:
commands:
- checkout
- sem-service start mysql 8
- sudo apt-get update
- sudo apt-get install -y libmysqlclient-dev
- cache restore
- gem install bundler
- bundle config set --local deployment 'true'
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ gem 'kaminari'

# Authentication and authorization
gem 'cancancan', '~> 3.0'
gem 'identity', ref: 'e18aa91', github: 'quintel/identity_rails'
gem 'identity', ref: 'd88af33', github: 'quintel/identity_rails'

gem 'activerecord-session_store'
gem 'sidekiq'
Expand Down
68 changes: 4 additions & 64 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,16 @@ GIT

GIT
remote: https://github.com/quintel/identity_rails.git
revision: e18aa910c7df04b60b839dc5f81df6487381284f
ref: e18aa91
revision: d88af3330404aac6fee7e3b027ec3096ec2bba8f
ref: d88af33
specs:
identity (0.1.0)
dry-configurable (>= 1.0)
dry-initializer (>= 3.1)
dry-types (~> 1.7)
dry-validation (>= 1.10)
faraday (>= 2)
omniauth (>= 2.1)
omniauth-rails_csrf_protection (~> 1.0)
omniauth_openid_connect (~> 0.4)
jwt (>= 2.5)
rails (>= 7.0.0)

GIT
Expand Down Expand Up @@ -163,9 +161,7 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
ast (2.4.2)
attr_required (1.0.2)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
Expand All @@ -177,7 +173,6 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
bigdecimal (3.1.9)
bindata (2.5.0)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
bootsnap (1.10.3)
Expand Down Expand Up @@ -263,8 +258,6 @@ GEM
dry-initializer (~> 3.0)
dry-schema (>= 1.12, < 2)
zeitwerk (~> 2.6)
email_validator (2.2.4)
activemodel
equalizer (0.0.11)
erb-formatter (0.7.3)
syntax_tree (~> 6.0)
Expand All @@ -279,8 +272,6 @@ GEM
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-net_http (3.4.0)
net-http (>= 0.5.0)
ffi (1.15.5)
Expand All @@ -296,7 +287,6 @@ GEM
temple (>= 0.8.2)
thor
tilt
hashie (5.0.0)
highline (2.0.3)
http-accept (1.7.0)
http-cookie (1.0.4)
Expand Down Expand Up @@ -325,13 +315,8 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.6.1)
json-jwt (1.16.7)
activesupport (>= 4.2)
aes_key_wrap
jwt (3.2.0)
base64
bindata
faraday (~> 2.0)
faraday-follow_redirects
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
Expand Down Expand Up @@ -407,29 +392,6 @@ GEM
nokogiri (1.18.5-x86_64-linux-gnu)
racc (~> 1.4)
numo-narray (0.9.2.1)
omniauth (2.1.2)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth_openid_connect (0.8.0)
omniauth (>= 1.9, < 3)
openid_connect (~> 2.2)
openid_connect (2.3.1)
activemodel
attr_required (>= 1.0.0)
email_validator
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.16)
mail
rack-oauth2 (~> 2.2)
swd (~> 2.0)
tzinfo
validate_url
webfinger (~> 2.0)
parallel (1.22.1)
parser (3.2.0.0)
ast (~> 2.4.1)
Expand All @@ -455,16 +417,6 @@ GEM
rack (2.2.13)
rack-cors (1.1.1)
rack (>= 2.0.0)
rack-oauth2 (2.2.1)
activesupport
attr_required
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-session (1.0.2)
rack (< 3)
rack-test (2.2.0)
Expand Down Expand Up @@ -606,11 +558,6 @@ GEM
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.5)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
faraday (~> 2.0)
faraday-follow_redirects
syntax_tree (6.2.0)
prettier_print (>= 1.2.0)
tailwindcss-rails (3.0.0)
Expand Down Expand Up @@ -641,9 +588,6 @@ GEM
unf_ext (0.0.8)
unicode-display_width (2.4.2)
uri (1.0.3)
validate_url (1.0.15)
activemodel (>= 3.0.0)
public_suffix
view_component (3.17.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
Expand All @@ -658,10 +602,6 @@ GEM
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (~> 4.0)
webfinger (2.1.3)
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webrick (1.9.1)
websocket (1.2.9)
websocket-driver (0.7.7)
Expand Down
21 changes: 4 additions & 17 deletions app/controllers/api/v3/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Api
module V3
class BaseController < ActionController::API
include ActionController::MimeResponds
include Identity::ResourceServer

rescue_from ActionController::ParameterMissing do |e|
render json: { errors: [e.message] }, status: :bad_request
Expand All @@ -26,10 +27,6 @@ class BaseController < ActionController::API
end
end

rescue_from ETEngine::TokenDecoder::DecodeError, JSON::JWT::Exception do
render json: { errors: ['Invalid or expired token'] }, status: :unauthorized
end

def set_current_scenario
@scenario = if params[:scenario_id]
Scenario.find(params[:scenario_id])
Expand All @@ -46,27 +43,17 @@ def process_action(*args)

private

# Returns the contents of the current token, if an Authorization header is set.
def token
return @token if @token
return nil if request.authorization.blank?

request.authorization.to_s.match(/\ABearer (.+)\z/) do |match|
return @token = ETEngine::TokenDecoder.decode(match[1])
end
end

# Returns the current user, if a token is set and is valid.
def current_user
return nil unless token
return nil unless decoded_token

@current_user ||= User.from_jwt!(token) if token
@current_user ||= User.from_jwt!(decoded_token)
end

def current_ability
@current_ability ||=
if current_user
TokenAbility.new(token, current_user)
TokenAbility.new(decoded_token, current_user)
else
GuestAbility.new
end
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def initialize_memory_cache
end

def current_user
@current_user ||= User.from_session_user!(identity_user) if signed_in?
@current_user ||=
(User.from_jwt!(identity_token) if identity_token)
rescue ActiveRecord::RecordNotFound
reset_session
redirect_to root_path
Expand Down
26 changes: 26 additions & 0 deletions app/javascript/controllers/session_keeper_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Controller } from "@hotwired/stimulus";
import { startSessionKeeper } from "identity/session_keeper";

// Connects to data-controller="session-keeper" on <body>. Mounted unconditionally (not gated on a
// logged-in user): the session-keeper's whole job is to recover a session whose access cookie lapsed,
// a state in which the server sees no current_user. The shared logic guards against guest reload
// loops, so an unconditional mount is safe. See identity/session_keeper in the identity gem.
export default class extends Controller {
// expCookie names the hint cookie the keeper times off; suffixed on deployments that share a
// cookie domain, so it comes from the server (Identity::ApplicationHelper) rather than assumed.
static values = {
idpUrl: String,
expCookie: { type: String, default: "etm_session_exp" },
};

connect() {
this.teardown = startSessionKeeper({
idpUrl: this.idpUrlValue,
expCookieName: this.expCookieValue,
});
}

disconnect() {
this.teardown?.();
}
}
37 changes: 14 additions & 23 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ class User < ApplicationRecord

attr_accessor :identity_user

delegate :roles, :admin?, to: :identity_user, allow_nil: true
delegate :roles, to: :identity_user, allow_nil: true
has_many :scenario_users, dependent: :destroy
has_many :scenarios, through: :scenario_users
has_many :scenario_version_tags
has_many :personal_access_tokens, dependent: :destroy

validates :name, presence: true

Expand Down Expand Up @@ -46,22 +45,11 @@ def admin?
identity_user&.admin? || admin
end

# Performs sign-in steps for an Identity::User.
#
# If a matching user exists in the database, it will be updated with the latest data from the
# Identity::User. Otherwise, a new user will be created.
#
# Returns the user. Raises an error if the user could not be saved.
def self.from_identity!(identity_user)
where(id: identity_user.id).first_or_initialize.tap do |user|
user.identity_user = identity_user
user.name = identity_user.name

user.save!
end
end

# Finds or creates a user from a JWT token.
#
# The token's claims are also set as identity_user: admin?/email/roles all prefer this fresh,
# per-request identity data over the persisted columns, which are only ever set at creation, so a
# role granted/revoked at the identity provider after that first login is still reflected here.
def self.from_jwt!(token)
id = token['sub']
admin = token.dig('user', 'admin')
Expand All @@ -70,7 +58,13 @@ def self.from_jwt!(token)

raise 'Token does not contain user information' if id.blank? || name.blank? || email.blank?

User.find_or_create_by!(id: token['sub']) do |u|
user = find_or_create_from_jwt(id:, admin:, name:, email:)
user&.identity_user = Identity::User.from_jwt_claims(token)
user
end

def self.find_or_create_from_jwt(id:, admin:, name:, email:)
User.find_or_create_by!(id: id) do |u|
u.admin = admin.presence || false
u.name = name
u.user_email = email
Expand All @@ -83,10 +77,7 @@ def self.from_jwt!(token)
# id.
# Also rescue from Deadlock: https://github.com/rails/rails/issues/54281
rescue ActiveRecord::RecordNotUnique, ActiveRecord::Deadlocked, ActiveRecord::LockWaitTimeout
User.find_by(id: token['sub'])
end

def self.from_session_user!(identity_user)
find(identity_user.id).tap { |u| u.identity_user = identity_user }
User.find_by(id: id)
end
private_class_method :find_or_create_from_jwt
end
2 changes: 1 addition & 1 deletion app/views/layouts/application.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
= stylesheet_link_tag 'application'
= javascript_importmap_tags 'inspect'

%body#data
%body#data{ data: identity_session_keeper_attributes }
.navbar.navbar-inverse
.navbar-inner
.container
Expand Down
5 changes: 5 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
# Enable hostname for puma-dev
config.hosts << 'etengine.test'

# Allow the ETLauncher cross-app parent domain so the shared etm_session cookie can be scoped to
# a dotted parent the browser accepts (a Domain cookie on .localhost is rejected). The parent is
# supplied by ETLauncher via ETM_HOST_PARENT (single source of truth); defaults to the local dev domain.
config.hosts << ENV.fetch('ETM_HOST_PARENT', '.local.energytransitionmodel.com')

# Always use a memory store so that we don't reload datasets on every request.
config.cache_store = :memory_store, { size: 512 * (1024**3) } # 512 Mb
# config.cache_store = :dalli_store
Expand Down
Loading