From e8c1a6066a1d296ba888c18c3bcb4b6c5712c2b9 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Tue, 8 Dec 2020 17:39:54 +0100 Subject: [PATCH 1/6] Move user db creation to service --- app/controllers/signup_controller.rb | 5 +++-- app/services/create_account.rb | 19 ++++++++++++++++--- spec/features/signup_spec.rb | 16 ++++++++++------ spec/services/create_account_spec.rb | 16 ++++++++++++++++ 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/app/controllers/signup_controller.rb b/app/controllers/signup_controller.rb index b332bbe..541c31d 100644 --- a/app/controllers/signup_controller.rb +++ b/app/controllers/signup_controller.rb @@ -94,14 +94,15 @@ class SignupController < ApplicationController end def complete_signup - @user.save! session[:new_user] = nil session[:validation_error] = nil CreateAccount.call( username: @user.cn, + domain: "kosmos.org", email: @user.email, - password: @user.password + password: @user.password, + invitation: @invitation ) @invitation.update! invited_user_id: @user.id, used_at: DateTime.now diff --git a/app/services/create_account.rb b/app/services/create_account.rb index 474427a..c4a3d58 100644 --- a/app/services/create_account.rb +++ b/app/services/create_account.rb @@ -1,16 +1,29 @@ class CreateAccount < ApplicationService def initialize(args) - @username = args[:username] - @email = args[:email] - @password = args[:password] + @username = args[:username] + @domain = args[:ou] || "kosmos.org" + @email = args[:email] + @password = args[:password] + @invitation = args[:invitation] end def call + create_user_in_database add_ldap_document end private + def create_user_in_database + User.create!( + cn: @username, + ou: @domain, + email: @email, + password: @password, + password_confirmation: @password + ) + end + def add_ldap_document dn = "cn=#{@username},ou=kosmos.org,cn=users,dc=kosmos,dc=org" attr = { diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index 09ebcc5..07cfa09 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -53,8 +53,11 @@ RSpec.describe "Signup", type: :feature do expect(page).to have_content("Choose a password") expect(CreateAccount).to receive(:call) - .with(username: "tony", email: "tony@example.com", password: "a-valid-password") - .and_return(true) + .with( + username: "tony", domain: "kosmos.org", + email: "tony@example.com", password: "a-valid-password", + invitation: Invitation.last + ).and_return(true) fill_in "user_password", with: "a-valid-password" click_button "Create account" @@ -62,7 +65,6 @@ RSpec.describe "Signup", type: :feature do expect(page).to have_content("confirm your address") end expect(page).to have_content("close this window or tab now") - expect(User.last.confirmed_at).to be_nil end scenario "Validation errors" do @@ -89,15 +91,17 @@ RSpec.describe "Signup", type: :feature do expect(page).to have_content("Password is too short") expect(CreateAccount).to receive(:call) - .with(username: "tony", email: "tony@example.com", password: "a-valid-password") - .and_return(true) + .with( + username: "tony", domain: "kosmos.org", + email: "tony@example.com", password: "a-valid-password", + invitation: Invitation.last + ).and_return(true) fill_in "user_password", with: "a-valid-password" click_button "Create account" within ".flash-msg.notice" do expect(page).to have_content("confirm your address") end - expect(User.last.cn).to eq("tony") end end end diff --git a/spec/services/create_account_spec.rb b/spec/services/create_account_spec.rb index dad16fb..c691f75 100644 --- a/spec/services/create_account_spec.rb +++ b/spec/services/create_account_spec.rb @@ -7,6 +7,22 @@ RSpec.describe CreateAccount, type: :model do allow(service).to receive(:ldap_client).and_return(ldap_client_mock) end + describe "#create_user_in_database" do + let(:service) { CreateAccount.new( + username: 'isaacnewton', + email: 'isaacnewton@example.com', + password: 'bright-ideas-in-autumn' + )} + + it "creates a new user record in the akkounts database" do + expect(User.count).to eq(0) + service.send(:create_user_in_database) + expect(User.count).to eq(1) + expect(User.last.cn).to eq("isaacnewton") + expect(User.last.email).to eq("isaacnewton@example.com") + end + end + describe "#add_ldap_document" do let(:service) { CreateAccount.new( username: 'halfinney', From 6dac732a7f3a647d70ad823c932ba0bce74b0969 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Tue, 8 Dec 2020 17:52:53 +0100 Subject: [PATCH 2/6] Move invitation invalidation to service --- app/controllers/signup_controller.rb | 2 -- app/services/create_account.rb | 7 ++++++- spec/services/create_account_spec.rb | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/controllers/signup_controller.rb b/app/controllers/signup_controller.rb index 541c31d..a355b75 100644 --- a/app/controllers/signup_controller.rb +++ b/app/controllers/signup_controller.rb @@ -104,7 +104,5 @@ class SignupController < ApplicationController password: @user.password, invitation: @invitation ) - - @invitation.update! invited_user_id: @user.id, used_at: DateTime.now end end diff --git a/app/services/create_account.rb b/app/services/create_account.rb index c4a3d58..e000631 100644 --- a/app/services/create_account.rb +++ b/app/services/create_account.rb @@ -8,8 +8,9 @@ class CreateAccount < ApplicationService end def call - create_user_in_database + user = create_user_in_database add_ldap_document + update_invitation(user.id) if @invitation.present? end private @@ -24,6 +25,10 @@ class CreateAccount < ApplicationService ) end + def update_invitation(user_id) + @invitation.update! invited_user_id: user_id, used_at: DateTime.now + end + def add_ldap_document dn = "cn=#{@username},ou=kosmos.org,cn=users,dc=kosmos,dc=org" attr = { diff --git a/spec/services/create_account_spec.rb b/spec/services/create_account_spec.rb index c691f75..8641beb 100644 --- a/spec/services/create_account_spec.rb +++ b/spec/services/create_account_spec.rb @@ -23,6 +23,28 @@ RSpec.describe CreateAccount, type: :model do end end + describe "#update_invitation" do + let(:invitation) { create :invitation } + let(:service) { CreateAccount.new( + username: 'isaacnewton', + email: 'isaacnewton@example.com', + password: 'bright-ideas-in-autumn', + invitation: invitation + )} + + before(:each) do + service.send(:update_invitation, 23) + end + + it "marks the invitation as used" do + expect(invitation.used_at).not_to be_nil + end + + it "saves the invited user's ID" do + expect(invitation.invited_user_id).to eq(23) + end + end + describe "#add_ldap_document" do let(:service) { CreateAccount.new( username: 'halfinney', From 54af949c7d4a629ed74c0ae195364efba19a5277 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Tue, 8 Dec 2020 18:08:36 +0100 Subject: [PATCH 3/6] Add faraday for HTTP requests --- Gemfile | 2 ++ Gemfile.lock | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Gemfile b/Gemfile index c31fab0..98b7e5f 100644 --- a/Gemfile +++ b/Gemfile @@ -28,6 +28,8 @@ gem 'devise' gem 'devise_ldap_authenticatable' gem 'net-ldap' +gem 'faraday' + group :development, :test do # Use sqlite3 as the database for Active Record gem 'sqlite3', '~> 1.4' diff --git a/Gemfile.lock b/Gemfile.lock index cc71186..cc3219c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -95,6 +95,8 @@ GEM factory_bot_rails (6.1.0) factory_bot (~> 6.1.0) railties (>= 5.0.0) + faraday (0.17.0) + multipart-post (>= 1.2, < 3) ffi (1.13.1) globalid (0.4.2) activesupport (>= 4.2.0) @@ -126,6 +128,7 @@ GEM mini_portile2 (2.4.0) minitest (5.14.2) msgpack (1.3.3) + multipart-post (2.1.1) net-ldap (0.16.3) nio4r (2.5.4) nokogiri (1.10.10) @@ -251,6 +254,7 @@ DEPENDENCIES devise_ldap_authenticatable dotenv-rails factory_bot_rails + faraday jbuilder (~> 2.7) letter_opener letter_opener_web From 8a0d89ef607f6f53580012ece0de4afecf8e8ddc Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Tue, 8 Dec 2020 18:15:44 +0100 Subject: [PATCH 4/6] Add webmock gem --- Gemfile | 1 + Gemfile.lock | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/Gemfile b/Gemfile index 98b7e5f..3895a98 100644 --- a/Gemfile +++ b/Gemfile @@ -53,6 +53,7 @@ group :test do gem 'factory_bot_rails' gem 'capybara' gem 'database_cleaner' + gem 'webmock' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index cc3219c..0fe560b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,6 +73,8 @@ GEM regexp_parser (~> 1.5) xpath (~> 3.2) concurrent-ruby (1.1.7) + crack (0.4.3) + safe_yaml (~> 1.0.0) crass (1.0.6) database_cleaner (1.8.5) devise (4.7.3) @@ -100,6 +102,7 @@ GEM ffi (1.13.1) globalid (0.4.2) activesupport (>= 4.2.0) + hashdiff (0.4.0) i18n (1.8.5) concurrent-ruby (~> 1.0) jbuilder (2.10.1) @@ -194,6 +197,7 @@ GEM rspec-mocks (~> 3.9) rspec-support (~> 3.9) rspec-support (3.10.0) + safe_yaml (1.0.5) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) @@ -231,6 +235,10 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + webmock (3.6.0) + addressable (>= 2.3.6) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) webpacker (4.3.0) activesupport (>= 4.2) rack-proxy (>= 0.6.1) @@ -272,6 +280,7 @@ DEPENDENCIES tzinfo-data warden web-console (>= 3.3.0) + webmock webpacker (~> 4.0) BUNDLED WITH From ee72a32c7ede6f5bdf6668bdfe37b53d360e29bd Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Tue, 8 Dec 2020 19:16:08 +0100 Subject: [PATCH 5/6] Exchange XMPP contacts when invitee signs up --- .env.example | 9 +------ .env.test | 1 + Gemfile | 2 +- app/services/create_account.rb | 25 +++++++++++++++++++- app/services/ejabberd_api_client.rb | 20 ++++++++++++++++ spec/services/create_account_spec.rb | 35 ++++++++++++++++++++++++++++ 6 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 .env.test create mode 100644 app/services/ejabberd_api_client.rb diff --git a/.env.example b/.env.example index c09dba1..5e587f2 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1 @@ -LDAP_HOST=192.168.33.10 -LDAP_PORT=389 -# -# Production LDAP server: -# -# LDAP_HOST=ldap.kosmos.org -# LDAP_PORT=636 -# LDAP_USE_TLS=true +EJABBERD_API_URL='https://xmpp.kosmos.org/api' diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..932c595 --- /dev/null +++ b/.env.test @@ -0,0 +1 @@ +EJABBERD_API_URL='http://xmpp.example.com/api' diff --git a/Gemfile b/Gemfile index 3895a98..8a5b728 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,7 @@ gem 'jbuilder', '~> 2.7' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.4.2', require: false -gem 'dotenv-rails', groups: [:development, :test] +gem 'dotenv-rails' gem 'warden' gem 'devise' diff --git a/app/services/create_account.rb b/app/services/create_account.rb index e000631..eb60824 100644 --- a/app/services/create_account.rb +++ b/app/services/create_account.rb @@ -10,7 +10,11 @@ class CreateAccount < ApplicationService def call user = create_user_in_database add_ldap_document - update_invitation(user.id) if @invitation.present? + + if @invitation.present? + update_invitation(user.id) + exchange_xmpp_contacts + end end private @@ -57,4 +61,23 @@ class CreateAccount < ApplicationService def ldap_config ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env] end + + def exchange_xmpp_contacts + #TODO enable in development when we have easy setup of ejabberd etc. + return if Rails.env.development? + + ejabberd = EjabberdApiClient.new + inviter = @invitation.user + + ejabberd.add_rosteritem({ + "localuser": @username, "localhost": @domain, + "user": inviter.cn, "host": inviter.ou, + "nick": inviter.cn, "group": "Friends", "subs": "both" + }) + ejabberd.add_rosteritem({ + "localuser": inviter.cn, "localhost": inviter.ou, + "user": @username, "host": @domain, + "nick": @username, "group": "Friends", "subs": "both" + }) + end end diff --git a/app/services/ejabberd_api_client.rb b/app/services/ejabberd_api_client.rb new file mode 100644 index 0000000..ddce590 --- /dev/null +++ b/app/services/ejabberd_api_client.rb @@ -0,0 +1,20 @@ +class EjabberdApiClient + def initialize + @base_url = ENV["EJABBERD_API_URL"] + end + + def post(endpoint, payload) + res = Faraday.post("#{@base_url}/#{endpoint}", payload, + "Content-Type" => "application/json") + + if res.status != 200 + Rails.logger.error "[ejabberd] API request failed:" + Rails.logger.error res.body + #TODO add some kind of exception tracking/notifications + end + end + + def add_rosteritem(payload) + post "add_rosteritem", payload + end +end diff --git a/spec/services/create_account_spec.rb b/spec/services/create_account_spec.rb index 8641beb..2808836 100644 --- a/spec/services/create_account_spec.rb +++ b/spec/services/create_account_spec.rb @@ -1,4 +1,6 @@ require 'rails_helper' +require 'webmock/rspec' +require 'json' RSpec.describe CreateAccount, type: :model do let(:ldap_client_mock) { instance_double(Net::LDAP) } @@ -68,4 +70,37 @@ RSpec.describe CreateAccount, type: :model do service.send(:add_ldap_document) end end + + describe "#exchange_xmpp_contacts" do + let(:inviter) { create :user, cn: "willherschel", ou: "kosmos.org" } + let(:invitation) { create :invitation, user: inviter } + let(:service) { CreateAccount.new( + username: 'isaacnewton', + email: 'isaacnewton@example.com', + password: 'bright-ideas-in-autumn', + invitation: invitation + )} + + before do + stub_request(:post, "http://xmpp.example.com/api/add_rosteritem") + .to_return(status: 200, body: "", headers: {}) + end + + it "posts add_rosteritem commands to the ejabberd API" do + service.send(:exchange_xmpp_contacts) + + expect(WebMock).to have_requested(:post, "http://xmpp.example.com/api/add_rosteritem") + .with { |req| req.body == { + localuser: "isaacnewton", localhost: "kosmos.org", + user: "willherschel", host: "kosmos.org", + nick: "willherschel", group: "Friends", subs: "both" + }} + expect(WebMock).to have_requested(:post, "http://xmpp.example.com/api/add_rosteritem") + .with { |req| req.body == { + localuser: "willherschel", localhost: "kosmos.org", + user: "isaacnewton", host: "kosmos.org", + nick: "isaacnewton", group: "Friends", subs: "both" + }} + end + end end From 69fc1ca57ec81044347131fa61f46780520486dc Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Tue, 8 Dec 2020 20:34:13 +0100 Subject: [PATCH 6/6] Add production dotenv config --- .env.production | 1 + 1 file changed, 1 insertion(+) create mode 100644 .env.production diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..5e587f2 --- /dev/null +++ b/.env.production @@ -0,0 +1 @@ +EJABBERD_API_URL='https://xmpp.kosmos.org/api'