From 19bafe081f9a88cd2ae27e073bb21fe1af64dd28 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A2u=20Cao?=
Date: Mon, 29 May 2023 23:05:18 +0200
Subject: [PATCH 1/4] Integrate Discourse Connect (SSO)
---
.env.example | 2 +
Gemfile | 3 ++
Gemfile.lock | 11 +++++
app/controllers/discourse/sso_controller.rb | 17 +++++++
app/models/setting.rb | 6 +++
.../settings/services/_discourse.html.erb | 47 ++++++++++++++++---
.../admin/settings/services/index.html.erb | 6 +++
app/views/devise/sessions/new.html.erb | 8 +++-
config/routes.rb | 4 ++
9 files changed, 97 insertions(+), 7 deletions(-)
create mode 100644 app/controllers/discourse/sso_controller.rb
diff --git a/.env.example b/.env.example
index 155ec8a..de85a10 100644
--- a/.env.example
+++ b/.env.example
@@ -19,6 +19,8 @@ LDAP_SUFFIX='dc=kosmos,dc=org'
WEBHOOKS_ALLOWED_IPS='10.1.1.163'
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
+DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
+
GITEA_PUBLIC_URL='https://gitea.kosmos.org'
MASTODON_PUBLIC_URL='https://kosmos.social'
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
diff --git a/Gemfile b/Gemfile
index 3445c32..dad66b1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -51,6 +51,9 @@ gem 'faraday'
gem 'sidekiq', '< 7'
gem 'sidekiq-scheduler'
+# Service integrations
+gem 'discourse_api'
+
# Monitoring
gem "sentry-ruby"
gem "sentry-rails"
diff --git a/Gemfile.lock b/Gemfile.lock
index 9a37e04..6b33fa8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -108,6 +108,11 @@ GEM
devise (>= 3.4.1)
net-ldap (>= 0.16.0)
diff-lcs (1.5.0)
+ discourse_api (2.0.0)
+ faraday (~> 2.7)
+ faraday-follow_redirects
+ faraday-multipart
+ rack (>= 1.6)
dotenv (2.8.1)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
@@ -126,6 +131,10 @@ GEM
faraday (2.7.1)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
+ faraday-follow_redirects (0.3.0)
+ faraday (>= 1, < 3)
+ faraday-multipart (1.0.4)
+ multipart-post (~> 2)
faraday-net_http (3.0.2)
ffi (1.15.5)
flipper (0.28.0)
@@ -183,6 +192,7 @@ GEM
mini_mime (1.1.2)
mini_portile2 (2.8.0)
minitest (5.16.3)
+ multipart-post (2.3.0)
net-imap (0.3.1)
net-protocol
net-ldap (0.17.1)
@@ -386,6 +396,7 @@ DEPENDENCIES
database_cleaner
devise (~> 4.9.0)
devise_ldap_authenticatable
+ discourse_api
dotenv-rails
factory_bot_rails
faker
diff --git a/app/controllers/discourse/sso_controller.rb b/app/controllers/discourse/sso_controller.rb
new file mode 100644
index 0000000..658f434
--- /dev/null
+++ b/app/controllers/discourse/sso_controller.rb
@@ -0,0 +1,17 @@
+class Discourse::SsoController < ApplicationController
+ before_action :authenticate_user!
+
+ def connect
+ secret = Setting.discourse_connect_secret
+ sso = DiscourseApi::SingleSignOn.parse(request.query_string, secret)
+ sso.external_id = current_user.id
+ sso.email = current_user.email
+ sso.username = current_user.cn
+ sso.name = current_user.display_name
+ sso.admin = current_user.is_admin?
+ sso.sso_secret = secret
+
+ redirect_to sso.to_url("#{Setting.discourse_public_url}/session/sso_login"),
+ allow_other_host: true
+ end
+end
diff --git a/app/models/setting.rb b/app/models/setting.rb
index 2cbc615..d25da65 100644
--- a/app/models/setting.rb
+++ b/app/models/setting.rb
@@ -2,6 +2,9 @@
class Setting < RailsSettings::Base
cache_prefix { "v1" }
+ field :accounts_domain, type: :string,
+ default: ENV["AKKOUNTS_DOMAIN"].presence
+
#
# Internal services
#
@@ -41,6 +44,9 @@ class Setting < RailsSettings::Base
field :discourse_enabled, type: :boolean,
default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
+ field :discourse_connect_secret, type: :string, readonly: true,
+ default: ENV["DISCOURSE_CONNECT_SECRET"].presence
+
#
# ejabberd
#
diff --git a/app/views/admin/settings/services/_discourse.html.erb b/app/views/admin/settings/services/_discourse.html.erb
index 498dd5f..6af5525 100644
--- a/app/views/admin/settings/services/_discourse.html.erb
+++ b/app/views/admin/settings/services/_discourse.html.erb
@@ -7,11 +7,46 @@
title: "Enable Discourse integration",
description: "Discourse configuration present and features enabled"
) %>
- <% if Setting.discourse_enabled? %>
- <%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
- <%= f.text_field :discourse_public_url,
- value: Setting.discourse_public_url,
- class: "w-full", disabled: true %>
- <% end %>
+<% if Setting.discourse_enabled? %>
+ <%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
+ <%= f.text_field :discourse_public_url,
+ value: Setting.discourse_public_url,
+ class: "w-full", disabled: true %>
<% end %>
+ <%= render FormElements::FieldsetComponent.new(title: "Connect secret") do %>
+ <%= f.password_field :discourse_connect_secret,
+ value: Setting.discourse_connect_secret,
+ class: "w-full", disabled: true %>
+ <% end %>
+<% end %>
+<% if Setting.discourse_enabled? %>
+ <% content_for :documentation do %>
+ How to configure Discourse
+
+ -
+ Set the Discourse Connect URL to the following URL:
+
+ -
+
+
+
+ -
+ Set the Discourse Connect Secret to the value above.
+
+ -
+ Enable Discourse Connect.
+
+ <% end %>
+<% end %>
diff --git a/app/views/admin/settings/services/index.html.erb b/app/views/admin/settings/services/index.html.erb
index 58360dd..f63653d 100644
--- a/app/views/admin/settings/services/index.html.erb
+++ b/app/views/admin/settings/services/index.html.erb
@@ -20,4 +20,10 @@
<% end %>
+
+ <% if content_for?(:documentation) %>
+
+ <%= yield :documentation %>
+
+ <% end %>
<% end %>
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb
index f7c0a70..aa964fb 100644
--- a/app/views/devise/sessions/new.html.erb
+++ b/app/views/devise/sessions/new.html.erb
@@ -1,7 +1,13 @@
+<%
+ # TODO remove when https://github.com/hotwired/turbo/issues/203 is fixed
+ enable_turbo = !session[:user_return_to].match?('/discourse/connect')
+%>
+
<%= render HeaderCompactComponent.new(title: "Log in") %>
<%= render MainCompactComponent.new do %>
- <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
+ <%= form_for(resource, as: resource_name, url: session_path(resource_name),
+ data: { turbo: enable_turbo.to_s }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<%= f.label :cn, 'User', class: 'block mb-2 font-bold' %>
diff --git a/config/routes.rb b/config/routes.rb
index aa7fac3..89986a3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -63,6 +63,10 @@ Rails.application.routes.draw do
get ".well-known/webfinger" => "webfinger#show"
+ namespace :discourse do
+ get "connect", to: 'sso#connect'
+ end
+
authenticate :user, ->(user) { user.is_admin? } do
mount Sidekiq::Web => '/sidekiq'
mount Flipper::UI.app(Flipper) => '/flipper'
From f829bb337907d4c0e61990f05ae5ef0e8d7e50ae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A2u=20Cao?=
Date: Mon, 29 May 2023 23:09:48 +0200
Subject: [PATCH 2/4] Use devise method for requiring login
---
app/controllers/account_controller.rb | 2 +-
app/controllers/contributions/donations_controller.rb | 2 +-
app/controllers/contributions/projects_controller.rb | 2 +-
app/controllers/dashboard_controller.rb | 2 +-
app/controllers/invitations_controller.rb | 2 +-
app/controllers/services/lightning_controller.rb | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb
index 182a5f8..7ee4d7d 100644
--- a/app/controllers/account_controller.rb
+++ b/app/controllers/account_controller.rb
@@ -1,5 +1,5 @@
class AccountController < ApplicationController
- before_action :require_user_signed_in
+ before_action :authenticate_user!
def index
@current_section = :account
diff --git a/app/controllers/contributions/donations_controller.rb b/app/controllers/contributions/donations_controller.rb
index 5839030..1533abb 100644
--- a/app/controllers/contributions/donations_controller.rb
+++ b/app/controllers/contributions/donations_controller.rb
@@ -1,5 +1,5 @@
class Contributions::DonationsController < ApplicationController
- before_action :require_user_signed_in
+ before_action :authenticate_user!
# GET /donations
# GET /donations.json
diff --git a/app/controllers/contributions/projects_controller.rb b/app/controllers/contributions/projects_controller.rb
index 77e9fdf..7989fef 100644
--- a/app/controllers/contributions/projects_controller.rb
+++ b/app/controllers/contributions/projects_controller.rb
@@ -1,5 +1,5 @@
class Contributions::ProjectsController < ApplicationController
- before_action :require_user_signed_in
+ before_action :authenticate_user!
# GET /contributions
def index
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 3f9c14e..d6234c6 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -1,5 +1,5 @@
class DashboardController < ApplicationController
- before_action :require_user_signed_in
+ before_action :authenticate_user!
def index
@current_section = :services
diff --git a/app/controllers/invitations_controller.rb b/app/controllers/invitations_controller.rb
index 3bb038a..0f1a778 100644
--- a/app/controllers/invitations_controller.rb
+++ b/app/controllers/invitations_controller.rb
@@ -1,5 +1,5 @@
class InvitationsController < ApplicationController
- before_action :require_user_signed_in, except: ["show"]
+ before_action :authenticate_user!, except: ["show"]
before_action :require_user_signed_out, only: ["show"]
# GET /invitations
diff --git a/app/controllers/services/lightning_controller.rb b/app/controllers/services/lightning_controller.rb
index a6b7381..4dd9607 100644
--- a/app/controllers/services/lightning_controller.rb
+++ b/app/controllers/services/lightning_controller.rb
@@ -1,7 +1,7 @@
require "rqrcode"
class Services::LightningController < ApplicationController
- before_action :require_user_signed_in
+ before_action :authenticate_user!
before_action :authenticate_with_lndhub
before_action :set_current_section
before_action :fetch_balance
From 745a319b3dd6e46da91297e44503c4c251db587f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A2u=20Cao?=
Date: Mon, 29 May 2023 23:10:39 +0200
Subject: [PATCH 3/4] Minor refactoring
---
app/views/admin/settings/services/_ejabberd.html.erb | 2 +-
config/routes.rb | 10 +++++-----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/app/views/admin/settings/services/_ejabberd.html.erb b/app/views/admin/settings/services/_ejabberd.html.erb
index 887c1c1..81490f9 100644
--- a/app/views/admin/settings/services/_ejabberd.html.erb
+++ b/app/views/admin/settings/services/_ejabberd.html.erb
@@ -19,7 +19,7 @@
class: "w-full", disabled: true %>
<% end %>
-User default settings
+User default settings
<%= render FormElements::FieldsetComponent.new(
title: "Default rooms",
diff --git a/config/routes.rb b/config/routes.rb
index 89986a3..684f386 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,7 +1,7 @@
require 'sidekiq/web'
Rails.application.routes.draw do
- devise_for :users, controllers: { confirmations: "users/confirmations" }
+ devise_for :users, controllers: { confirmations: 'users/confirmations' }
get 'welcome', to: 'welcome#index'
get 'check_your_email', to: 'welcome#check_your_email'
@@ -61,20 +61,20 @@ Rails.application.routes.draw do
end
end
- get ".well-known/webfinger" => "webfinger#show"
+ get ".well-known/webfinger", to: 'webfinger#show'
namespace :discourse do
get "connect", to: 'sso#connect'
end
authenticate :user, ->(user) { user.is_admin? } do
- mount Sidekiq::Web => '/sidekiq'
- mount Flipper::UI.app(Flipper) => '/flipper'
+ mount Sidekiq::Web, at: '/sidekiq'
+ mount Flipper::UI.app(Flipper), at: '/flipper'
end
# Letter Opener (open "sent" emails in dev and staging)
if Rails.env.match(/staging|development/)
- mount LetterOpenerWeb::Engine, at: "letter_opener"
+ mount LetterOpenerWeb::Engine, at: '/letter_opener'
end
root to: 'dashboard#index'
From 7e05530ab7b3188f6bd59ddd458f212aa0124de3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A2u=20Cao?=
Date: Tue, 30 May 2023 23:19:49 +0200
Subject: [PATCH 4/4] Add specs for Discourse Connect
---
.env.test | 3 +++
spec/requests/discourse/sso_spec.rb | 41 +++++++++++++++++++++++++++++
2 files changed, 44 insertions(+)
create mode 100644 spec/requests/discourse/sso_spec.rb
diff --git a/.env.test b/.env.test
index 016655b..31947dd 100644
--- a/.env.test
+++ b/.env.test
@@ -1,3 +1,6 @@
+DISCOURSE_PUBLIC_URL='http://discourse.example.com'
+DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
+
EJABBERD_API_URL='http://xmpp.example.com/api'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
diff --git a/spec/requests/discourse/sso_spec.rb b/spec/requests/discourse/sso_spec.rb
new file mode 100644
index 0000000..7cbb0a9
--- /dev/null
+++ b/spec/requests/discourse/sso_spec.rb
@@ -0,0 +1,41 @@
+require 'rails_helper'
+require 'webmock/rspec'
+
+RSpec.describe "Discourse SSO", type: :request do
+
+ describe "GET /discourse/connect" do
+ let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' }
+
+ before do
+ Warden.test_mode!
+ login_as user, scope: :user
+ allow(user).to receive(:display_name).and_return('Jimbo')
+ allow(user).to receive(:is_admin?).and_return(false)
+ end
+
+ after do
+ Warden.test_reset!
+ end
+
+ context "with invalid SSO credentials" do
+ it "results in a failed signature check" do
+ expect {
+ get discourse_connect_path(
+ sso: "bm9uY2U9ODk2N2NiMmFlZTdlMjdjNzZiZTNkZWQ5ODIwYzMzN2QmcmV0dXJuX3Nzb191cmw9aHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTMwMDAlMkZzZXNzaW9uJTJGc3NvX2xvZ2lu",
+ sig: "01fc008ff7b51855217e879b6f14aaddefbbd4df2d128951f7bb70cfde834c2a"
+ )
+ }.to raise_error(DiscourseApi::SingleSignOn::ParseError)
+ end
+ end
+
+ context "valid SSO credentials" do
+ it "redirects to the Discourse SSO endpoint" do
+ get discourse_connect_path(
+ sso: "bm9uY2U9YjQwYWZmYzg0YWQ2NWE1ZTk5MjdlZWU1NWEzMjdhMTQmcmV0dXJuX3Nzb191cmw9aHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTMwMDAlMkZzZXNzaW9uJTJGc3NvX2xvZ2lu",
+ sig: "b7905c5db612391293249ad5272dac493681efcd255133f6c2aff91ba654a319"
+ )
+ expect(response).to redirect_to('http://discourse.example.com/session/sso_login?sso=YWRtaW49ZmFsc2UmZW1haWw9amltbXklNDBleGFtcGxlLmNvbSZleHRlcm5hbF9pZD0xJm5hbWU9SmltYm8mbm9uY2U9YjQwYWZmYzg0YWQ2NWE1ZTk5MjdlZWU1NWEzMjdhMTQmcmV0dXJuX3Nzb191cmw9aHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTMwMDAlMkZzZXNzaW9uJTJGc3NvX2xvZ2luJnVzZXJuYW1lPWppbW15&sig=d5f8b1d6db66569bef789fda4a3216119c2d42b84725d043c9a57dde1e528842')
+ end
+ end
+ end
+end