Skip to content

Commit

Permalink
Maintenance: Extend desktop view login tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
mgruner committed Apr 5, 2024
1 parent 64a3de9 commit 106f9d9
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 216 deletions.
2 changes: 1 addition & 1 deletion .gitlab/ci/__includes__/services.yml
Expand Up @@ -101,7 +101,7 @@
alias: ci-service-proxy

auth:
name: $CI_REGISTRY/docker/zammad-auth:develop
name: $CI_REGISTRY/docker/zammad-auth:stable
alias: ci-service-auth

mattermost:
Expand Down
4 changes: 2 additions & 2 deletions .rubocop/default.yml
Expand Up @@ -373,7 +373,7 @@ RSpec/DescribeMethod:
RSpec/NestedGroups:
Max: 6

RSpec/Rails/AvoidSetupHook:
RSpecRails/AvoidSetupHook:
Exclude:
- "test/**/*"

Expand All @@ -388,7 +388,7 @@ RSpec/AnyInstance:
RSpec/SubjectStub:
Enabled: false

RSpec/Rails/InferredSpecType:
RSpecRails/InferredSpecType:
Description: 'Identifies redundant spec type.'
Enabled: false # We use types to add DSL to rspec.

Expand Down
76 changes: 76 additions & 0 deletions spec/support/saml.rb
@@ -0,0 +1,76 @@
# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/

require 'keycloak/admin'

module ZammadSpecSupportSAML

def saml_configure_keycloak(zammad_saml_metadata:, saml_client_json:)
# Setup Keycloak SAML authentication.
if !Keycloak::Admin.configured?
Keycloak::Admin.configure do |config|
config.username = ENV['KEYCLOAK_ADMIN_USER']
config.password = ENV['KEYCLOAK_ADMIN_PASSWORD']
config.realm = 'zammad'
config.base_url = ENV['KEYCLOAK_BASE_URL']
end
end

# Force create Zammad client in Keycloak.
client = Keycloak::Admin.clients.lookup(clientId: zammad_saml_metadata)
if client.count.positive?
Keycloak::Admin.clients.delete(client.first['id'])
end
Keycloak::Admin.clients.create(JSON.parse(saml_client_json))
end

def saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, name_identifier_format: nil, uid_attribute: nil, idp_slo_service_url: true, security: nil)
# Setup Zammad SAML authentication.
response = UserAgent.get(saml_realm_zammad_descriptor)
raise 'No Zammad realm descriptor found' if !response.success?

match = response.body.match(%r{<ds:X509Certificate>(?<cert>.+)</ds:X509Certificate>})
raise 'No X509Certificate found' if !match[:cert]

auth_saml_credentials =
{
display_name: 'SAML',
idp_sso_target_url: "#{saml_base_url}/realms/zammad/protocol/saml",
idp_cert: "-----BEGIN CERTIFICATE-----\n#{match[:cert]}\n-----END CERTIFICATE-----",
name_identifier_format: name_identifier_format || 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
}
auth_saml_credentials[:idp_slo_service_url] = "#{saml_base_url}/realms/zammad/protocol/saml" if idp_slo_service_url
auth_saml_credentials[:uid_attribute] = uid_attribute if uid_attribute

if security.present?
auth_saml_credentials[:security] = 'on'
auth_saml_credentials[:certificate] = "-----BEGIN CERTIFICATE-----\n#{security[:cert]}\n-----END CERTIFICATE-----"
auth_saml_credentials[:private_key] = "-----BEGIN RSA PRIVATE KEY-----\n#{security[:key]}\n-----END RSA PRIVATE KEY-----" # gitleaks:allow
auth_saml_credentials[:private_key_secret] = ''
end

# Disable setting validation. We have an explicit test for this.
setting = Setting.find_by(name: 'auth_saml_credentials')
setting.state_current = { value: auth_saml_credentials }
setting.save!(validate: false)

Setting.set('auth_saml', true)
end

def saml_login_keycloak
find_by_id('kc-form')
expect(page).to have_current_path(%r{/realms/zammad/protocol/saml\?SAMLRequest=.+})
expect(page).to have_css('#kc-form-login')

within '#kc-form-login' do
fill_in 'username', with: 'john.doe'
fill_in 'password', with: 'test'

click_on 'Sign In'
end
end

end

RSpec.configure do |config|
config.include ZammadSpecSupportSAML, integration_standalone: :saml
end
81 changes: 57 additions & 24 deletions spec/system/apps/desktop/login_spec.rb
Expand Up @@ -3,40 +3,73 @@
require 'rails_helper'

RSpec.describe 'Desktop > Login', app: :desktop_view, authenticated_as: false, type: :system do
it 'Login with remember me and logout again' do
visit '/login', skip_waiting: true
context 'when logging in with two factor auth' do
let(:user) { User.find_by(login: 'admin@example.com') }
let(:code) { two_factor_pref.configuration[:code] }
let(:recover_code_enabled) { false }
let!(:two_factor_pref) { create(:user_two_factor_preference, :authenticator_app, user:) }
let(:token) { 'token' }

login(
username: 'admin@example.com',
password: 'test',
remember_me: true,
)
before do
visit '/login'

expect_current_route '/'
login(
username: 'admin@example.com',
password: 'test',
remember_me: true,
skip_waiting: true,
)
end

refresh
it 'can login with correct code' do
expect(page).to have_no_text('Try another method')

cookie = cookie('^_zammad.+?')
expect(cookie[:expires]).to be_truthy
find_input('Security Code').type(code)
find_button('Sign in').click

logout
expect_current_route 'login'
expect(page).to have_text('Logout')

# Check that cookies has no longer a expire date after logout.
cookie = cookie('^_zammad.+?')
expect(cookie[:expires]).to be_nil
logout
expect(page).to have_text('Sign in')
end
end

it 'Login and redirect to requested url' do
visit '/playground', skip_waiting: true
context 'when loggin in via external authentication provider', authenticated_as: false, integration: true, integration_standalone: :saml, required_envs: %w[KEYCLOAK_BASE_URL KEYCLOAK_ADMIN_USER KEYCLOAK_ADMIN_PASSWORD] do
let(:zammad_base_url) { "#{Capybara.app_host}:#{Capybara.current_session.server.port}" }
let(:zammad_saml_metadata) { "#{zammad_base_url}/auth/saml/metadata" }
let(:saml_base_url) { ENV['KEYCLOAK_BASE_URL'] }
let(:saml_client_json) { Rails.root.join('test/data/saml/zammad-client.json').read.gsub('#ZAMMAD_BASE_URL', zammad_base_url) }
let(:saml_realm_zammad_descriptor) { "#{saml_base_url}/realms/zammad/protocol/saml/descriptor" }
let(:saml_realm_zammad_accounts) { "#{saml_base_url}/realms/zammad/account" }

expect_current_route '/login?redirect=/playground' # TODO: FIX route to valid desktop view route instead of playground
before do
saml_configure_keycloak(zammad_saml_metadata:, saml_client_json:)
saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
end

login(
username: 'admin@example.com',
password: 'test',
)
it 'can login via external authentication provider' do
visit '/login'
expect(page).to have_text('Or sign in using')
expect(page).to have_text('SAML')

expect_current_route '/playground'
find_button('SAML').click

saml_login_keycloak

# Workaround: SAML redirects in CI don't work because of missing HTTP referrer headers.
visit '/'
expect(page).to have_text('Logout')

# Manual logout
click_button 'Logout' # rubocop:disable Capybara/ClickLinkOrButtonStyle
expect(page).to have_current_path(%r{/login})
wait_for_test_flag('applicationLoaded.loaded', skip_clearing: true)

visit '/'
expect_current_route '/login'

visit saml_realm_zammad_accounts
expect(page).to have_text('Sign in')
end
end
end
132 changes: 37 additions & 95 deletions spec/system/apps/mobile/login_spec.rb
Expand Up @@ -3,90 +3,15 @@
require 'rails_helper'

RSpec.describe 'Mobile > Login', app: :mobile, authenticated_as: false, type: :system do
it 'Login with remember me and logout again' do
visit '/login', skip_waiting: true

login(
username: 'admin@example.com',
password: 'test',
remember_me: true,
)

expect_current_route '/'

refresh

cookie = cookie('^_zammad.+?')
expect(cookie[:expires]).to be_truthy

logout
expect_current_route 'login'

# Check that cookies has no longer a expire date after logout.
cookie = cookie('^_zammad.+?')
expect(cookie[:expires]).to be_nil
end

it 'Login and redirect to requested url' do
visit '/notifications', skip_waiting: true

expect_current_route '/login?redirect=/notifications'

login(
username: 'admin@example.com',
password: 'test',
)

expect_current_route '/notifications'
end

it 'Shows public links' do
link = create(:public_link)
visit '/login', skip_waiting: true

wait_for_gql('shared/entities/public-links/graphql/queries/links.graphql')

expect(page).to have_link(link.title, href: link.link)

link.update!(title: 'new link')

wait_for_gql('shared/entities/public-links/graphql/subscriptions/currentLinks.graphql')

expect(page).to have_link('new link', href: link.link)
end

context 'when after auth is required' do
it 'requires setting up two factor auth' do
allow_any_instance_of(Auth::AfterAuth::TwoFactorConfiguration).to receive(:check).and_return(true)

visit '/login', skip_waiting: true

login(
username: 'admin@example.com',
password: 'test',
remember_me: true,
skip_waiting: true,
)

expect(page).to have_content('The two-factor authentication is not configured yet')
expect_current_route '/login/after-auth'
end
end

context 'when logging in with two factor auth' do
let(:user) { User.find_by(login: 'admin@example.com') }
let(:code) { two_factor_pref.configuration[:code] }
let(:recover_code_enabled) { false }
let!(:two_factor_pref) { create(:user_two_factor_preference, :authenticator_app, user:) }
let(:token) { 'token' }
let(:recovery_2fa) { create(:user_two_factor_preference, :recovery_codes, recovery_code: token, user:) }

before do
stub_const('Auth::BRUTE_FORCE_SLEEP', 0)
recovery_2fa if recover_code_enabled
Setting.set('two_factor_authentication_recovery_codes', recover_code_enabled)

visit '/login', skip_waiting: true
visit '/login'

login(
username: 'admin@example.com',
Expand All @@ -102,33 +27,50 @@
find_input('Security Code').type(code)
find_button('Sign in').click

expect_current_route '/'
expect(page).to have_text('Home')

logout
expect_current_route '/login'
end
end

context 'when loggin in via external authentication provider', authenticated_as: false, integration: true, integration_standalone: :saml, required_envs: %w[KEYCLOAK_BASE_URL KEYCLOAK_ADMIN_USER KEYCLOAK_ADMIN_PASSWORD] do
let(:zammad_base_url) { "#{Capybara.app_host}:#{Capybara.current_session.server.port}" }
let(:zammad_saml_metadata) { "#{zammad_base_url}/auth/saml/metadata" }
let(:saml_base_url) { ENV['KEYCLOAK_BASE_URL'] }
let(:saml_client_json) { Rails.root.join('test/data/saml/zammad-client.json').read.gsub('#ZAMMAD_BASE_URL', zammad_base_url) }
let(:saml_realm_zammad_descriptor) { "#{saml_base_url}/realms/zammad/protocol/saml/descriptor" }
let(:saml_realm_zammad_accounts) { "#{saml_base_url}/realms/zammad/account" }

before do
saml_configure_keycloak(zammad_saml_metadata:, saml_client_json:)
saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
end

context 'when logging in with recovery code' do
let(:recover_code_enabled) { true }
it 'can login via external authentication provider' do
visit '/login'
expect(page).to have_text('Or sign in using')
expect(page).to have_text('SAML')

before do
find_button('Try another method').click
find_button('Or use one of your recovery codes.').click
end
find_button('SAML').click

it 'can login with correct code' do
find_input('Recovery Code').type(token)
find_button('Sign in').click
saml_login_keycloak

expect_current_route '/'
end
# Workaround: SAML redirects in CI don't work because of missing HTTP referrer headers.
visit '/'
expect(page).to have_text('Home')

it 'shows an error with incorrect code' do
find_input('Recovery Code').type('incorrect')
find_button('Sign in').click
# Manual logout
click_on 'JD' # avatar
click_on 'Sign out'
expect(page).to have_current_path(%r{/login})
wait_for_test_flag('applicationLoaded.loaded', skip_clearing: true)

expect(page).to have_text('Please double-check your two-factor authentication method')
visit '/'
expect_current_route '/login'

expect_current_route '/login'
end
visit saml_realm_zammad_accounts
expect(page).to have_text('Sign in')
end
end

end

0 comments on commit 106f9d9

Please sign in to comment.