Merge pull request 'Exchange XMPP contacts when invitee signs up' (#13) from feature/automatic_xmpp_roster into master
continuous-integration/drone/push Build is passing Details

Reviewed-on: #13
This commit is contained in:
Râu Cao 2020-12-09 20:52:14 +00:00
commit 602ca6ee94
10 changed files with 170 additions and 22 deletions

View File

@ -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'

1
.env.production Normal file
View File

@ -0,0 +1 @@
EJABBERD_API_URL='https://xmpp.kosmos.org/api'

1
.env.test Normal file
View File

@ -0,0 +1 @@
EJABBERD_API_URL='http://xmpp.example.com/api'

View File

@ -21,13 +21,15 @@ 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'
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'
@ -51,6 +53,7 @@ group :test do
gem 'factory_bot_rails'
gem 'capybara'
gem 'database_cleaner'
gem 'webmock'
end
group :production do

View File

@ -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)
@ -95,9 +97,12 @@ 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)
hashdiff (0.4.0)
i18n (1.8.5)
concurrent-ruby (~> 1.0)
jbuilder (2.10.1)
@ -126,6 +131,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)
@ -191,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)
@ -228,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)
@ -251,6 +262,7 @@ DEPENDENCIES
devise_ldap_authenticatable
dotenv-rails
factory_bot_rails
faraday
jbuilder (~> 2.7)
letter_opener
letter_opener_web
@ -268,6 +280,7 @@ DEPENDENCIES
tzinfo-data
warden
web-console (>= 3.3.0)
webmock
webpacker (~> 4.0)
BUNDLED WITH

View File

@ -94,16 +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
end
end

View File

@ -1,16 +1,38 @@
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
user = create_user_in_database
add_ldap_document
if @invitation.present?
update_invitation(user.id)
exchange_xmpp_contacts
end
end
private
def create_user_in_database
User.create!(
cn: @username,
ou: @domain,
email: @email,
password: @password,
password_confirmation: @password
)
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 = {
@ -39,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

View File

@ -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

View File

@ -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

View File

@ -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) }
@ -7,6 +9,44 @@ 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 "#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',
@ -30,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