diff --git a/LICENSE-ICONS-3RD-PARTY.json b/LICENSE-ICONS-3RD-PARTY.json index 48d19536c..c36e26b64 100644 --- a/LICENSE-ICONS-3RD-PARTY.json +++ b/LICENSE-ICONS-3RD-PARTY.json @@ -688,5 +688,10 @@ "author": "Felix Niklas", "url": "", "license": "MIT" + }, + "sso-button.svg": { + "author": "Tanu Doank", + "url": "https://thenounproject.com/term/key/1247931/", + "license": "CC 3.0 Attribution" } } \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee b/app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee index d8ee745ef..f3556ff27 100644 --- a/app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee +++ b/app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee @@ -111,4 +111,9 @@ App.Config.set('auth_provider_all', { name: 'SAML' config: 'auth_saml' class: 'saml' + sso: + url: '/auth/sso' + name: 'SSO' + config: 'auth_sso' + class: 'sso' }) diff --git a/app/assets/stylesheets/svg-dimensions.css b/app/assets/stylesheets/svg-dimensions.css index 11bd7d184..10d16a36e 100644 --- a/app/assets/stylesheets/svg-dimensions.css +++ b/app/assets/stylesheets/svg-dimensions.css @@ -114,6 +114,7 @@ .icon-sms { width: 17px; height: 17px; } .icon-spinner-small { width: 15px; height: 15px; } .icon-split { width: 16px; height: 17px; } +.icon-sso-button { width: 29px; height: 24px; } .icon-status-modified-outer-circle { width: 16px; height: 16px; } .icon-status { width: 16px; height: 16px; } .icon-stopwatch { width: 77px; height: 83px; } diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index 47774006a..bae70f740 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -3199,6 +3199,10 @@ ol.tabs li { background: hsl(0,0%,27%); } + &.auth-provider--sso { + background: #454545; + } + .provider-name { flex: 1; } @@ -3399,6 +3403,10 @@ ol.tabs li { fill: white; } +.icon-sso-button { + fill: white; +} + /* * removed margin of forms to not break the layout with submit buttons within
area e. g. for modal dialogs diff --git a/app/controllers/application_controller/authenticates.rb b/app/controllers/application_controller/authenticates.rb index 5e3ac287b..3e1ae4378 100644 --- a/app/controllers/application_controller/authenticates.rb +++ b/app/controllers/application_controller/authenticates.rb @@ -141,21 +141,6 @@ module ApplicationController::Authenticates authentication_check_prerequesits(user, 'session', {}) end - def authenticate_with_sso - user = begin - login = request.env['REMOTE_USER'] || - request.env['HTTP_REMOTE_USER'] || - request.headers['X-Forwarded-User'] - User.lookup(login: login&.downcase) - end - - raise Exceptions::NotAuthorized, 'Missing SSO ENV REMOTE_USER' if login.blank? - raise Exceptions::NotAuthorized, "No such user #{login} from ENV REMOTE_USER" if !user - - session.delete(:switched_from_user_id) - authentication_check_prerequesits(user, 'SSO', {}) - end - def authentication_check_prerequesits(user, auth_type, auth_param) raise Exceptions::NotAuthorized, 'Maintenance mode enabled!' if in_maintenance_mode?(user) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index d298f2b0e..4cca6b9b7 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -16,7 +16,21 @@ class SessionsController < ApplicationController end def create_sso - authenticate_with_sso + raise Exceptions::NotAuthorized, 'SSO authentication disabled!' if !Setting.get('auth_sso') + + user = begin + login = request.env['REMOTE_USER'] || + request.env['HTTP_REMOTE_USER'] || + request.headers['X-Forwarded-User'] + + User.lookup(login: login&.downcase) + end + + raise Exceptions::NotAuthorized, 'Missing SSO ENV REMOTE_USER or X-Forwarded-User header' if login.blank? + raise Exceptions::NotAuthorized, "No such user '#{login}' found!" if user.blank? + + session.delete(:switched_from_user_id) + authentication_check_prerequesits(user, 'SSO', {}) redirect_to '/#' end diff --git a/contrib/apache2/zammad.conf b/contrib/apache2/zammad.conf index 03a26e095..347ae5a09 100644 --- a/contrib/apache2/zammad.conf +++ b/contrib/apache2/zammad.conf @@ -31,6 +31,9 @@ ProxyPass /ws ws://127.0.0.1:6042/ ProxyPass / http://127.0.0.1:3000/ + # change this line in an SSO setup + RequestHeader unset X-Forwarded-User + DocumentRoot "/opt/zammad/public" diff --git a/contrib/apache2/zammad_ssl.conf b/contrib/apache2/zammad_ssl.conf index 24d482ef4..361acb0b4 100644 --- a/contrib/apache2/zammad_ssl.conf +++ b/contrib/apache2/zammad_ssl.conf @@ -54,6 +54,9 @@ ProxyPass /ws ws://127.0.0.1:6042/ ProxyPass / http://127.0.0.1:3000/ + # change this line in an SSO setup + RequestHeader unset X-Forwarded-User + # Use settings below if proxying does not work and you receive HTTP-Errror 404 # if you use the settings below, make sure to comment out the above two options # This may not apply to all systems, applies to openSuse diff --git a/contrib/nginx/zammad.conf b/contrib/nginx/zammad.conf index 836164574..e51198017 100644 --- a/contrib/nginx/zammad.conf +++ b/contrib/nginx/zammad.conf @@ -46,6 +46,10 @@ server { proxy_set_header CLIENT_IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + + # Change this line in an SSO setup + proxy_set_header X-Forwarded-User ""; + proxy_read_timeout 300; proxy_pass http://zammad-railsserver; diff --git a/contrib/nginx/zammad_ssl.conf b/contrib/nginx/zammad_ssl.conf index 60349efb9..2e8e4bc54 100644 --- a/contrib/nginx/zammad_ssl.conf +++ b/contrib/nginx/zammad_ssl.conf @@ -100,6 +100,10 @@ server { proxy_set_header Host $http_host; proxy_set_header CLIENT_IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # change this line in an SSO setup + proxy_set_header X-Forwarded-User ""; + proxy_read_timeout 180; proxy_pass http://zammad-railsserver; diff --git a/db/migrate/20200724130426_issue3128_add_sso.rb b/db/migrate/20200724130426_issue3128_add_sso.rb new file mode 100644 index 000000000..00b7d8018 --- /dev/null +++ b/db/migrate/20200724130426_issue3128_add_sso.rb @@ -0,0 +1,38 @@ +class Issue3128AddSso < ActiveRecord::Migration[5.2] + def change + + # return if it's a new setup + return if !Setting.exists?(name: 'system_init_done') + + Setting.create_if_not_exists( + title: 'Authentication via %s', + name: 'auth_sso', + area: 'Security::ThirdPartyAuthentication', + description: 'Enables button for user authentication via %s. The button will redirect to /auth/sso on user interaction.', + options: { + form: [ + { + display: '', + null: true, + name: 'auth_sso', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + preferences: { + controller: 'SettingsAreaSwitch', + sub: {}, + title_i18n: ['SSO'], + description_i18n: ['SSO', 'Button for Single Sign On.'], + permission: ['admin.security'], + }, + state: false, + frontend: true + ) + + end +end diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 9e8418c6d..03bc77e4f 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -4613,3 +4613,33 @@ Setting.create_if_not_exists( }, frontend: true, ) + +Setting.create_if_not_exists( + title: 'Authentication via %s', + name: 'auth_sso', + area: 'Security::ThirdPartyAuthentication', + description: 'Enables button for user authentication via %s. The button will redirect to /auth/sso on user interaction.', + options: { + form: [ + { + display: '', + null: true, + name: 'auth_sso', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + preferences: { + controller: 'SettingsAreaSwitch', + sub: {}, + title_i18n: ['SSO'], + description_i18n: ['SSO', 'Button for Single Sign On.'], + permission: ['admin.security'], + }, + state: false, + frontend: true +) diff --git a/public/assets/images/icons.svg b/public/assets/images/icons.svg index f117ee617..1f6d347f3 100644 --- a/public/assets/images/icons.svg +++ b/public/assets/images/icons.svg @@ -763,6 +763,9 @@ split + + + status-modified-outer-circle diff --git a/public/assets/images/icons/sso-button.svg b/public/assets/images/icons/sso-button.svg new file mode 100644 index 000000000..8d4869187 --- /dev/null +++ b/public/assets/images/icons/sso-button.svg @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + clip-rule="evenodd" + fill-rule="evenodd" + y="0px" + x="0px" + viewBox="0 0 96.57 79.92" + style="image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" + version="1.1" + xml:space="preserve" + fill="#50e3c2" + width="29" + height="24"> + <defs + id="defs24" /> + <g + transform="matrix(0.33139046,0,0,0.33139046,-6.8223822,-15.05759)" + id="g30" + fill="#50e3c2"> + <path + id="path26" + d="m 52,172 c 0,-5 4,-9 10,-9 5,0 9,4 9,9 0,5 -4,9 -9,9 -6,0 -10,-4 -10,-9 z m 66,-14 c 0,-4 3,-7 7,-7 4,0 7,3 7,7 0,4 -3,6 -7,6 -4,0 -7,-2 -7,-6 z m -23,0 c 0,-2 2,-4 5,-4 2,0 4,2 4,4 0,2 -2,4 -4,4 -3,0 -5,-2 -5,-4 z m 55,6 c -3,0 -6,-2 -6,-6 0,-4 3,-7 6,-7 4,0 7,3 7,7 0,4 -3,6 -7,6 z m -25,17 c -2,0 -4,-2 -4,-4 0,-3 2,-5 4,-5 2,0 4,2 4,5 0,2 -2,4 -4,4 z m 62,-14 c 0,-8 2,-16 4,-23 2,-4 3,-8 5,-11 h -26 c -2,0 -3,2 -3,4 v 7 H 60 c -15,0 -29,8 -38,23 9,14 23,22 38,22 h 107 v 8 c 0,2 1,3 3,3 h 26 c -2,-3 -3,-7 -5,-11 -2,-7 -4,-14 -4,-22 z" /> + <path + id="path28" + d="m 282,167 c 0,7 -7,14 -14,14 -8,0 -15,-7 -15,-14 0,-8 7,-14 15,-14 7,0 14,6 14,14 z m 29,0 c 0,-33 -26,-59 -58,-59 -22,0 -41,12 -51,29 -5,9 -8,19 -8,30 0,10 3,20 8,29 10,17 29,29 51,29 32,0 58,-26 58,-58 z" /> + </g> +</svg> + diff --git a/spec/requests/session_spec.rb b/spec/requests/session_spec.rb index aec5c6f97..c91958e0a 100644 --- a/spec/requests/session_spec.rb +++ b/spec/requests/session_spec.rb @@ -30,6 +30,27 @@ RSpec.describe 'Sessions endpoints', type: :request do end describe 'GET /auth/sso (single sign-on)' do + + before do + Setting.set('auth_sso', true) + end + + context 'when SSO is disabled' do + + before do + Setting.set('auth_sso', false) + end + + let(:headers) { { 'X-Forwarded-User' => login } } + let(:login) { User.last.login } + + it 'returns a new user-session response' do + get '/auth/sso', as: :json, headers: headers + + expect(response).to have_http_status(:unauthorized) + end + end + context 'with invalid user login' do let(:login) { User.pluck(:login).max.next }