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

+
    +
  1. + Set the Discourse Connect URL to the following URL: +
  2. +
  3. + + +
  4. +
  5. + Set the Discourse Connect Secret to the value above. +
  6. +
  7. + Enable Discourse Connect. +
  8. + <% 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