From d4258b84740c4901b69841b7bace28977d85cb1f Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Thu, 20 Jan 2022 18:33:27 +0100 Subject: [PATCH 01/16] Add omniauth dependency Signed-off-by: Andrej Shadura --- src/api/Gemfile | 3 +++ src/api/Gemfile.lock | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/src/api/Gemfile b/src/api/Gemfile index c1a70df6ec..80869b46fb 100644 --- a/src/api/Gemfile +++ b/src/api/Gemfile @@ -85,6 +85,9 @@ gem 'deep_cloneable', '~> 2.4.0' # Server-side datatables gem 'ajax-datatables-rails' +# SSO +gem 'omniauth' + group :development, :production do # to have the delayed job daemon gem 'daemons' diff --git a/src/api/Gemfile.lock b/src/api/Gemfile.lock index 90e9c5b421..c62e1770ff 100644 --- a/src/api/Gemfile.lock +++ b/src/api/Gemfile.lock @@ -180,6 +180,7 @@ GEM rubocop (>= 0.50.0) sysexits (~> 1.1) hashdiff (0.4.0) + hashie (5.0.0) html2haml (2.2.0) erubis (~> 2.7.0) haml (>= 4.0, < 6) @@ -252,6 +253,10 @@ GEM racc (~> 1.4) nokogumbo (2.0.1) nokogiri (~> 1.8, >= 1.8.4) + omniauth (2.0.4) + hashie (>= 3.4.6) + rack (>= 1.6.2, < 3) + rack-protection parallel (1.17.0) parser (2.6.3.0) ast (~> 2.4.0) @@ -290,6 +295,8 @@ GEM activesupport (>= 3.0.0) racc (1.5.2) rack (2.2.3) + rack-protection (2.1.0) + rack rack-test (1.1.0) rack (>= 1.0, < 3) rails (5.2.6.2) @@ -532,6 +539,7 @@ DEPENDENCIES mousetrap-rails mysql2 nokogiri + omniauth peek peek-dalli peek-host -- GitLab From a9e2a6586014edb1fd63876afa098c527692a274 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Wed, 26 Jan 2022 13:46:09 +0100 Subject: [PATCH 02/16] Add dependency on omniauth-gitlab Signed-off-by: Andrej Shadura --- src/api/Gemfile | 1 + src/api/Gemfile.lock | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/api/Gemfile b/src/api/Gemfile index 80869b46fb..0e90885b62 100644 --- a/src/api/Gemfile +++ b/src/api/Gemfile @@ -87,6 +87,7 @@ gem 'ajax-datatables-rails' # SSO gem 'omniauth' +gem 'omniauth-gitlab' group :development, :production do # to have the delayed job daemon diff --git a/src/api/Gemfile.lock b/src/api/Gemfile.lock index c62e1770ff..215c9a9837 100644 --- a/src/api/Gemfile.lock +++ b/src/api/Gemfile.lock @@ -154,6 +154,29 @@ GEM tty-pager (~> 0.12.0) tty-screen (~> 0.6.5) tty-tree (~> 0.3.0) + faraday (1.9.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.3) + multipart-post (>= 1.2, < 3) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) feature (1.4.0) ffi (1.11.1) flot-rails (0.0.7) @@ -201,6 +224,7 @@ GEM jquery-ui-rails (4.2.1) railties (>= 3.2.16) json (2.5.1) + jwt (2.3.0) kaminari (1.2.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.1) @@ -246,6 +270,9 @@ GEM momentjs-rails (2.20.1) railties (>= 3.1) mousetrap-rails (1.4.6) + multi_json (1.15.0) + multi_xml (0.6.0) + multipart-post (2.1.1) mysql2 (0.5.2) nio4r (2.5.8) nokogiri (1.11.7) @@ -253,10 +280,22 @@ GEM racc (~> 1.4) nokogumbo (2.0.1) nokogiri (~> 1.8, >= 1.8.4) + oauth2 (1.4.7) + faraday (>= 0.8, < 2.0) + jwt (>= 1.0, < 3.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) omniauth (2.0.4) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) rack-protection + omniauth-gitlab (3.0.0) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.7.1) + omniauth-oauth2 (1.7.2) + oauth2 (~> 1.4) + omniauth (>= 1.9, < 3) parallel (1.17.0) parser (2.6.3.0) ast (~> 2.4.0) @@ -384,6 +423,7 @@ GEM rubocop (>= 0.60.0) ruby-ldap (0.9.20) ruby-progressbar (1.10.1) + ruby2_keywords (0.0.5) ruby_parser (3.13.1) sexp_processor (~> 4.9) rubyzip (2.0.0) @@ -540,6 +580,7 @@ DEPENDENCIES mysql2 nokogiri omniauth + omniauth-gitlab peek peek-dalli peek-host -- GitLab From 8efa7b76dbaac9c5d19a7b97ed9c27df03a69cb3 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Wed, 26 Jan 2022 21:52:49 +0100 Subject: [PATCH 03/16] Add omniauth-rails_csrf_protection Signed-off-by: Andrej Shadura --- src/api/Gemfile | 1 + src/api/Gemfile.lock | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/api/Gemfile b/src/api/Gemfile index 0e90885b62..d355553048 100644 --- a/src/api/Gemfile +++ b/src/api/Gemfile @@ -88,6 +88,7 @@ gem 'ajax-datatables-rails' # SSO gem 'omniauth' gem 'omniauth-gitlab' +gem 'omniauth-rails_csrf_protection' group :development, :production do # to have the delayed job daemon diff --git a/src/api/Gemfile.lock b/src/api/Gemfile.lock index 215c9a9837..7b82ef3841 100644 --- a/src/api/Gemfile.lock +++ b/src/api/Gemfile.lock @@ -296,6 +296,9 @@ GEM omniauth-oauth2 (1.7.2) oauth2 (~> 1.4) omniauth (>= 1.9, < 3) + omniauth-rails_csrf_protection (1.0.0) + actionpack (>= 4.2) + omniauth (~> 2.0) parallel (1.17.0) parser (2.6.3.0) ast (~> 2.4.0) @@ -581,6 +584,7 @@ DEPENDENCIES nokogiri omniauth omniauth-gitlab + omniauth-rails_csrf_protection peek peek-dalli peek-host -- GitLab From f9f7e2a74dd319a1a7ccd25c79d0b3c8ad4b3912 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Thu, 27 May 2021 13:43:22 +0200 Subject: [PATCH 04/16] Parse SSO authentication config on startup Signed-off-by: Andrej Shadura --- src/api/config/auth.yml.example | 8 ++++++++ src/api/config/initializers/omniauth.rb | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/api/config/auth.yml.example create mode 100644 src/api/config/initializers/omniauth.rb diff --git a/src/api/config/auth.yml.example b/src/api/config/auth.yml.example new file mode 100644 index 0000000000..c5a9ce375e --- /dev/null +++ b/src/api/config/auth.yml.example @@ -0,0 +1,8 @@ +fdo-gitlab: + strategy: gitlab + description: Freedesktop.org GitLab + scope: read_user openid profile email + client_id: hexhexhexhex + client_secret: hexhexhexhex + client_options: + site: https://gitlab.freedesktop.org/api/v4 diff --git a/src/api/config/initializers/omniauth.rb b/src/api/config/initializers/omniauth.rb new file mode 100644 index 0000000000..4ae910ecfd --- /dev/null +++ b/src/api/config/initializers/omniauth.rb @@ -0,0 +1,20 @@ +OmniAuth.config.path_prefix = '/session/sso' + +path = Rails.root.join("config", "auth.yml") + +CONFIG['sso_auth'] = Hash.new + +if File.exist? path + begin + CONFIG['sso_auth'] = YAML.load_file(path) + rescue Exception + puts "Error while parsing config file #{path}" + end + + Rails.application.config.middleware.use OmniAuth::Builder do + CONFIG['sso_auth'].each do |name, options| + options[:name] = name + provider (options['strategy'] || name), options + end + end +end -- GitLab From 1c8943502ec275c14e54887b319c81d6ad635ae1 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Mon, 17 May 2021 20:25:34 +0200 Subject: [PATCH 05/16] Add an SSO login page so that users can choose between providers Signed-off-by: Andrej Shadura --- src/api/app/controllers/webui/session_controller.rb | 6 +++++- src/api/app/views/layouts/webui2/_login_form.html.haml | 8 +++++++- src/api/app/views/webui/session/_form.html.haml | 6 ++++++ src/api/app/views/webui2/webui/session/_sso.html.haml | 8 ++++++++ src/api/app/views/webui2/webui/session/new.html.haml | 7 +++++-- src/api/app/views/webui2/webui/session/sso.html.haml | 6 ++++++ src/api/config/initializers/omniauth.rb | 1 + src/api/config/routes.rb | 2 ++ 8 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 src/api/app/views/webui2/webui/session/_sso.html.haml create mode 100644 src/api/app/views/webui2/webui/session/sso.html.haml diff --git a/src/api/app/controllers/webui/session_controller.rb b/src/api/app/controllers/webui/session_controller.rb index a6a0032a22..dee1597723 100644 --- a/src/api/app/controllers/webui/session_controller.rb +++ b/src/api/app/controllers/webui/session_controller.rb @@ -1,7 +1,7 @@ class Webui::SessionController < Webui::WebuiController before_action :kerberos_auth, only: [:new] - skip_before_action :check_anonymous, only: [:create] + skip_before_action :check_anonymous, only: [:new, :create, :sso] def new switch_to_webui2 @@ -40,6 +40,10 @@ class Webui::SessionController < Webui::WebuiController redirect_on_logout end + def sso + switch_to_webui2 + end + private def redirect_on_login diff --git a/src/api/app/views/layouts/webui2/_login_form.html.haml b/src/api/app/views/layouts/webui2/_login_form.html.haml index 981f9329bb..504bb75155 100644 --- a/src/api/app/views/layouts/webui2/_login_form.html.haml +++ b/src/api/app/views/layouts/webui2/_login_form.html.haml @@ -6,7 +6,7 @@ Log In .dropdown-menu.dropdown-menu-right.shadow-lg.bg-dark{ 'aria-labelledby': 'dropdownMenuButton' } .px-4.py-3#login-form - = form_tag(form_url, options) do + = form_tag(form_url, options.merge({class: 'pb-3'})) do - if proxy = hidden_field_tag(:context, 'default') = hidden_field_tag(:proxypath, 'reserve') @@ -21,3 +21,9 @@ .float-right = submit_tag('Log In', class: 'btn btn-success') .clearfix + .form-group.border-top.pt-3 + %p{class: 'text-light'} Or sign in with: + .form-inline + - CONFIG['sso_auth'].each do |name, options| + = form_tag "#{sso_path}/#{name}", method: 'post', class: 'px-1' do + = button_tag options['description'], class: 'btn btn-light' diff --git a/src/api/app/views/webui/session/_form.html.haml b/src/api/app/views/webui/session/_form.html.haml index 95901a4a3b..2d0c15c1f2 100644 --- a/src/api/app/views/webui/session/_form.html.haml +++ b/src/api/app/views/webui/session/_form.html.haml @@ -9,6 +9,12 @@ %input#user-password{ name: "password", size: "30", type: "password" }/ %p %input.primary#log-in-button{ name: "login", type: "submit", value: "Log In" }/ +- if CONFIG['sso_auth'] + %p + Sign in using one of the following external services: + %p + - CONFIG['sso_auth'].each do |name, options| + = button_tag "Log in with #{options['description']}", formaction: "#{sso_path}/#{name}" %p - if CONFIG['proxy_auth_mode'] == :on Or diff --git a/src/api/app/views/webui2/webui/session/_sso.html.haml b/src/api/app/views/webui2/webui/session/_sso.html.haml new file mode 100644 index 0000000000..32d4c98f25 --- /dev/null +++ b/src/api/app/views/webui2/webui/session/_sso.html.haml @@ -0,0 +1,8 @@ +%h3= 'Sign in with SSO' + +.form-group + %p Sign in with your external account: + .form-inline + - CONFIG['sso_auth'].each do |name, options| + = form_tag "#{sso_path}/#{name}", method: 'post', class: 'px-1' do + = button_tag options['description'], class: 'btn btn-outline-dark' diff --git a/src/api/app/views/webui2/webui/session/new.html.haml b/src/api/app/views/webui2/webui/session/new.html.haml index 7ac2406543..3d8e681d86 100644 --- a/src/api/app/views/webui2/webui/session/new.html.haml +++ b/src/api/app/views/webui2/webui/session/new.html.haml @@ -1,8 +1,8 @@ - @pagetitle = 'Please Log In' .card - .card-body#loginform - .col-lg-6.pl-0 + .card-body.row#loginform + .col-lg-6.pl-3 - if CONFIG['proxy_auth_mode'] == :on = render partial: 'form', locals: { form_url: CONFIG['proxy_auth_login_page'], options: { method: :post, enctype: 'application/x-www-form-urlencoded' }, @@ -17,3 +17,6 @@ options: { method: :post }, proxy: false, pagetitle: @pagetitle } + - if CONFIG['sso_auth'] + .col-lg-6.pl-3.border-left + = render partial: 'sso' diff --git a/src/api/app/views/webui2/webui/session/sso.html.haml b/src/api/app/views/webui2/webui/session/sso.html.haml new file mode 100644 index 0000000000..f78e2a98c6 --- /dev/null +++ b/src/api/app/views/webui2/webui/session/sso.html.haml @@ -0,0 +1,6 @@ +- @pagetitle = 'Sign In with SSO' + +.card + .card-body#loginform + .col-lg-6.pl-0 + = render partial: 'sso' diff --git a/src/api/config/initializers/omniauth.rb b/src/api/config/initializers/omniauth.rb index 4ae910ecfd..5d8f90823c 100644 --- a/src/api/config/initializers/omniauth.rb +++ b/src/api/config/initializers/omniauth.rb @@ -15,6 +15,7 @@ if File.exist? path CONFIG['sso_auth'].each do |name, options| options[:name] = name provider (options['strategy'] || name), options + options['description'] ||= OmniAuth::Utils.camelize(name) end end end diff --git a/src/api/config/routes.rb b/src/api/config/routes.rb index d1b40af86d..7af7b8d8ae 100644 --- a/src/api/config/routes.rb +++ b/src/api/config/routes.rb @@ -424,6 +424,8 @@ OBSApi::Application.routes.draw do controller 'webui/session' do get 'session/new' => :new post 'session/create' => :create + get 'session/sso' => :sso, as: 'sso' + get 'session/sso/:provider/callback' => :sso_callback delete 'session/destroy' => :destroy end -- GitLab From 731382460f49ebf4fd7335505eaa5aebbe7039b8 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Thu, 27 May 2021 13:34:57 +0200 Subject: [PATCH 06/16] Rename create_ldap_user to create_external_user Signed-off-by: Andrej Shadura --- src/api/app/models/user.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/api/app/models/user.rb b/src/api/app/models/user.rb index e545dcbf6b..663ad00d50 100644 --- a/src/api/app/models/user.rb +++ b/src/api/app/models/user.rb @@ -164,12 +164,12 @@ class User < ApplicationRecord create!(attributes.merge(password: SecureRandom.base64(48))) end - def self.create_ldap_user(attributes = {}) - user = create_user_with_fake_pw!(attributes.merge(state: default_user_state, adminnote: 'User created via LDAP')) + def self.create_external_user(attributes = {}) + user = create_user_with_fake_pw!(attributes.merge(state: default_user_state)) return user if user.errors.empty? - logger.info("Cannot create ldap userid: '#{login}' on OBS. Full log: #{user.errors.full_messages.to_sentence}") + logger.info("Cannot create external userid: '#{login}' on OBS. Full log: #{user.errors.full_messages.to_sentence}") return end @@ -214,7 +214,10 @@ class User < ApplicationRecord logger.debug("Email: #{ldap_info[0]}") logger.debug("Name : #{ldap_info[1]}") - user = create_ldap_user(login: login, email: ldap_info[0], realname: ldap_info[1]) + user = create_external_user(login: login, + email: ldap_info[0], + realname: ldap_info[1], + adminnote: "User created via LDAP") end user.mark_login! -- GitLab From aa61b3419863fc6613d48082e339b17b1ff96479 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Thu, 27 May 2021 13:47:32 +0200 Subject: [PATCH 07/16] user model: add find_with_omniauth/create_with_omniauth Signed-off-by: Andrej Shadura --- src/api/app/models/user.rb | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/api/app/models/user.rb b/src/api/app/models/user.rb index 663ad00d50..3ba3c214f3 100644 --- a/src/api/app/models/user.rb +++ b/src/api/app/models/user.rb @@ -224,6 +224,33 @@ class User < ApplicationRecord user end + def self.find_with_omniauth(auth) + if auth + email = auth['info']['email'] + user = find_by_email(email) + if user + user.mark_login! + + return user + end + end + end + + def self.create_with_omniauth(auth, login) + provider = CONFIG['sso_auth'][auth['provider']]['description'] + email = auth['info']['email'] + logger.debug("Creating OmniAuth user for #{provider}") + logger.debug("Email: #{email}") + logger.debug("Name : #{auth['info']['name']}") + + user = create_external_user(login: login, + email: email, + realname: auth['info']['name'], + deprecated_password_hash_type: 'invalid', + adminnote: "User created via #{provider}") + user.mark_login! + user + end # Currently logged in user or nobody user if there is no user logged in. # Use this to check permissions, but don't treat it as logged in user. Check # is_nobody? on the returned object -- GitLab From 83cfd4860f8f48b9e399b9b4999b98a7098720a0 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Tue, 18 May 2021 15:05:57 +0000 Subject: [PATCH 08/16] Add SSO callback to allow existing users log in with an external source Signed-off-by: Andrej Shadura --- .../controllers/webui/session_controller.rb | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/api/app/controllers/webui/session_controller.rb b/src/api/app/controllers/webui/session_controller.rb index dee1597723..7f38136e15 100644 --- a/src/api/app/controllers/webui/session_controller.rb +++ b/src/api/app/controllers/webui/session_controller.rb @@ -1,7 +1,7 @@ class Webui::SessionController < Webui::WebuiController before_action :kerberos_auth, only: [:new] - skip_before_action :check_anonymous, only: [:new, :create, :sso] + skip_before_action :check_anonymous, only: [:new, :create, :sso, :sso_callback] def new switch_to_webui2 @@ -44,6 +44,29 @@ class Webui::SessionController < Webui::WebuiController switch_to_webui2 end + def sso_callback + @auth_hash = request.env['omniauth.auth'] + user = User.find_with_omniauth(@auth_hash) + + unless user + RabbitmqBus.send_to_bus('metrics', 'login,access_point=webui,failure=unauthenticated value=1') + redirect_to(session_new_path, error: 'Authentication failed') + return + end + + unless user.is_active? + RabbitmqBus.send_to_bus('metrics', 'login,access_point=webui,failure=disabled value=1') + redirect_to(root_path, error: 'Your account is disabled. Please contact the administrator for details.') + return + end + + User.session = user + session[:login] = user.login + Rails.logger.debug "Authenticated user '#{user.login}'" + + redirect_on_login + end + private def redirect_on_login -- GitLab From 90626a8187e624d2c59f29c82b9def3bf8ffd919 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Wed, 26 Jan 2022 14:39:29 +0100 Subject: [PATCH 09/16] Reenable SSO Signed-off-by: Andrej Shadura --- Dockerfile.frontend-base | 1 + docker/frontend-docker-entrypoint.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile.frontend-base b/Dockerfile.frontend-base index add3446e6d..b57ead0e96 100644 --- a/Dockerfile.frontend-base +++ b/Dockerfile.frontend-base @@ -33,6 +33,7 @@ RUN apt-get update \ ruby-bundler \ ruby-ffi \ sphinxsearch \ + python3-yaml \ supervisor \ time \ tzdata diff --git a/docker/frontend-docker-entrypoint.sh b/docker/frontend-docker-entrypoint.sh index 5f1a1c008d..8fbbcfd0f4 100755 --- a/docker/frontend-docker-entrypoint.sh +++ b/docker/frontend-docker-entrypoint.sh @@ -10,7 +10,7 @@ chmod a+rwxt /tmp /opt/configure-app.sh /opt/configure-db.sh -#/opt/configure-sso.py +/opt/configure-sso.py : ${OBS_FRONTEND_WORKERS:=4} export OBS_FRONTEND_WORKERS -- GitLab From b964a4186652e79464362f48970aaa56006b496c Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Thu, 27 May 2021 13:42:39 +0200 Subject: [PATCH 10/16] Implement first login flow Signed-off-by: Andrej Shadura --- .../controllers/webui/session_controller.rb | 65 ++++++++++++++++++- .../webui/session/sso_confirm.html.haml | 17 +++++ src/api/config/routes.rb | 3 +- 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 src/api/app/views/webui2/webui/session/sso_confirm.html.haml diff --git a/src/api/app/controllers/webui/session_controller.rb b/src/api/app/controllers/webui/session_controller.rb index 7f38136e15..ca4027a87f 100644 --- a/src/api/app/controllers/webui/session_controller.rb +++ b/src/api/app/controllers/webui/session_controller.rb @@ -1,7 +1,7 @@ class Webui::SessionController < Webui::WebuiController before_action :kerberos_auth, only: [:new] - skip_before_action :check_anonymous, only: [:new, :create, :sso, :sso_callback] + skip_before_action :check_anonymous, only: [:new, :create, :sso, :sso_callback, :sso_confirm, :do_sso_confirm] def new switch_to_webui2 @@ -49,8 +49,8 @@ class Webui::SessionController < Webui::WebuiController user = User.find_with_omniauth(@auth_hash) unless user - RabbitmqBus.send_to_bus('metrics', 'login,access_point=webui,failure=unauthenticated value=1') - redirect_to(session_new_path, error: 'Authentication failed') + session[:auth] = @auth_hash + redirect_to(sso_confirm_path) return end @@ -67,6 +67,65 @@ class Webui::SessionController < Webui::WebuiController redirect_on_login end + def sso_confirm + switch_to_webui2 + auth_hash = session[:auth] + + if !auth_hash + redirect_to sso_path + return + end + + @derived_username = auth_hash['info']['username'] || + auth_hash['info']['nickname'] || + auth_hash['info']['email'].rpartition("@")[0] || + auth_hash['info']['name'].gsub(' ', '_') + end + + def do_sso_confirm + required_parameters :login + auth_hash = session[:auth] + + if !auth_hash + redirect_to sso_path + return + end + + existing_user = User.find_by_login(params[:login]) + if existing_user + flash[:error] = "Username #{params[:login]} is already taken, choose a different one" + redirect_to sso_confirm_path + return + end + + begin + user = User.create_with_omniauth(auth_hash, params[:login]) + rescue ActiveRecord::ActiveRecordError + flash[:error] = "Invalid username, please try a different one" + redirect_to sso_confirm_path + return + end + + unless user + flash[:error] = "Cannot create user" + redirect_to root_path + return + end + + unless user.is_active? + RabbitmqBus.send_to_bus('metrics', 'login,access_point=webui,failure=disabled value=1') + redirect_to(root_path, error: 'Your account needs to be confirmed by the administrator.') + return + end + + User.session = user + session[:login] = user.login + Rails.logger.debug "Authenticated user '#{user.login}'" + + redirect_on_login + end + + private def redirect_on_login diff --git a/src/api/app/views/webui2/webui/session/sso_confirm.html.haml b/src/api/app/views/webui2/webui/session/sso_confirm.html.haml new file mode 100644 index 0000000000..04ea69daac --- /dev/null +++ b/src/api/app/views/webui2/webui/session/sso_confirm.html.haml @@ -0,0 +1,17 @@ +- @pagetitle = 'First Login' + +.card + .card-body#loginform + .col-lg-6.pl-0 + - if can_register + %h3= @pagetitle + %p Since this is your first time you sign in, you need to choose your username. + = form_tag({ controller: 'session', action: 'sso_confirm', method: :post }, class: 'sign-up', autocomplete: 'off') do + .form-group + = label_tag 'login', 'Username:' + %abbr.text-danger{ title: 'required' } * + = text_field_tag 'login', @derived_username, placeholder: 'Username', autocomplete: 'off', class: 'form-control', required: true + = submit_tag('Confirm and Log In', class: 'btn btn-primary') + - else + %p Sorry, only existing users can sign in. + diff --git a/src/api/config/routes.rb b/src/api/config/routes.rb index 7af7b8d8ae..7b0b648a07 100644 --- a/src/api/config/routes.rb +++ b/src/api/config/routes.rb @@ -426,6 +426,8 @@ OBSApi::Application.routes.draw do post 'session/create' => :create get 'session/sso' => :sso, as: 'sso' get 'session/sso/:provider/callback' => :sso_callback + get 'session/sso/:provider/confirm' => :sso_confirm, as: 'sso_confirm' + post 'session/sso/:provider/confirm' => :do_sso_confirm delete 'session/destroy' => :destroy end @@ -503,7 +505,6 @@ OBSApi::Application.routes.draw do match 'build/:project/:repository' => 'build#index', constraints: cons, via: [:get, :post] match 'build/:project' => 'build#project_index', constraints: cons, via: [:get, :post, :put] get 'build' => 'source#index' - ### /published # :arch can be also a ymp for a pattern :/ -- GitLab From 960c56af3e84399af4fd3f0aacf23be5a241c3cf Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Tue, 8 Jun 2021 11:07:16 +0200 Subject: [PATCH 11/16] Try harder to derive the username from email addresses Some providers set username or nickname to an email address. For this reason, first collect the best possible user name we can find, and only then fix it to match our requirements. Signed-off-by: Andrej Shadura --- src/api/app/controllers/webui/session_controller.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/api/app/controllers/webui/session_controller.rb b/src/api/app/controllers/webui/session_controller.rb index ca4027a87f..66f9a44148 100644 --- a/src/api/app/controllers/webui/session_controller.rb +++ b/src/api/app/controllers/webui/session_controller.rb @@ -76,10 +76,19 @@ class Webui::SessionController < Webui::WebuiController return end + # Try to derive a username from the information available, + # falling back to full name if nothing else works @derived_username = auth_hash['info']['username'] || auth_hash['info']['nickname'] || - auth_hash['info']['email'].rpartition("@")[0] || - auth_hash['info']['name'].gsub(' ', '_') + auth_hash['info']['email'] || + auth_hash['info']['name'] + + # Some providers set username or nickname to an email address + # Derive the username from the local part of the email address, + # if possible. The full name with spaces replaced by underscores + # is the last resort fallback. + @derived_username = @derived_username.rpartition("@")[0] if @derived_username.include? "@" + @derived_username = @derived_username.gsub(' ', '_') end def do_sso_confirm -- GitLab From 8ebe1e5a30e9ef18420fe50aed9e82b878ef021d Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Thu, 27 Jan 2022 18:38:39 +0100 Subject: [PATCH 12/16] Verify the referrer URL when redirecting back The redirect should only happen to the referrer URL pointing at our domain, never to the external ones. Signed-off-by: Andrej Shadura --- .../controllers/webui/session_controller.rb | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/api/app/controllers/webui/session_controller.rb b/src/api/app/controllers/webui/session_controller.rb index 66f9a44148..29973ef7cb 100644 --- a/src/api/app/controllers/webui/session_controller.rb +++ b/src/api/app/controllers/webui/session_controller.rb @@ -138,7 +138,9 @@ class Webui::SessionController < Webui::WebuiController private def redirect_on_login - if referer_was_login? + if !referer_was_ours? + redirect_to root_path + elsif referer_was_login? redirect_to user_show_path(User.session!) else redirect_back(fallback_location: root_path) @@ -149,11 +151,24 @@ class Webui::SessionController < Webui::WebuiController if CONFIG['proxy_auth_mode'] == :on redirect_to CONFIG['proxy_auth_logout_page'] else - redirect_back(fallback_location: root_path) + redirect_to root_path end end + def referer_was_ours? + return false unless request.referer + + parsed = URI.parse(request.referer) + parsed.host == request.host and parsed.port == request.port + end + def referer_was_login? - request.referer && request.referer.end_with?(session_new_path) + return false unless request.referer + + parsed = URI.parse(request.referer) + return false unless parsed.host == request.host + return false unless parsed.port == request.port + + parsed.path == session_new_path or parsed.path.starts_with?(sso_path) end end -- GitLab From 0c8d82ff4c83068f6a686b7c880d7a989503fd0d Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Tue, 15 Jun 2021 14:32:30 +0200 Subject: [PATCH 13/16] Mark passwords for SSO-only users as invalid to allow changing them later MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new "hash type" for invalid passwords, which is never equal to normal passwords, but nevertheless can be changed without being known by the user. This "invalid" password can only be set by directly setting the password hash type. When updating the password using update_password method, it will always be upgrade it to the strongest hash type, sha256crypt. To allow changing this "invalid" password to a normal one, stop requiring a non-empty current password in the password change dialog when changing a password from an "invalid" one. Don’t show the current password box either, as it is not used anyway in this case, making it better not to show it to avoid confusion. Signed-off-by: Andrej Shadura --- src/api/app/controllers/webui/user_controller.rb | 3 ++- src/api/app/models/user.rb | 4 ++++ .../views/webui2/webui/user/_password_dialog.html.haml | 8 +++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/api/app/controllers/webui/user_controller.rb b/src/api/app/controllers/webui/user_controller.rb index 6c143616b2..aaa84aabad 100644 --- a/src/api/app/controllers/webui/user_controller.rb +++ b/src/api/app/controllers/webui/user_controller.rb @@ -153,9 +153,10 @@ class Webui::UserController < Webui::WebuiController return end - if user.authenticate(params[:password]) + if user.authenticate(params[:password]) or user.password_invalid? user.password = params[:new_password] user.password_confirmation = params[:repeat_password] + user.deprecated_password_hash_type = nil if user.save flash[:success] = 'Your password has been changed successfully.' diff --git a/src/api/app/models/user.rb b/src/api/app/models/user.rb index 3ba3c214f3..fd21734188 100644 --- a/src/api/app/models/user.rb +++ b/src/api/app/models/user.rb @@ -900,6 +900,10 @@ class User < ApplicationRecord end end + def password_invalid? + self.deprecated_password_hash_type == 'invalid' + end + private # The currently logged in user (might be nil). It's reset after diff --git a/src/api/app/views/webui2/webui/user/_password_dialog.html.haml b/src/api/app/views/webui2/webui/user/_password_dialog.html.haml index e3b100b309..534300e03a 100644 --- a/src/api/app/views/webui2/webui/user/_password_dialog.html.haml +++ b/src/api/app/views/webui2/webui/user/_password_dialog.html.haml @@ -5,9 +5,11 @@ %h5.modal-title#branch-modal-label Change Your Password = form_tag(action: 'change_password') do .modal-body - .form-group - = label_tag :password, 'Current Password:' - = text_field_tag :password, nil, type: 'password', required: 'true', class: 'form-control' + - if User.session + - unless User.session.password_invalid? + .form-group + = label_tag :password, 'Current Password:' + = text_field_tag :password, nil, type: 'password', required: 'true', class: 'form-control' .form-group = label_tag :new_password, 'New Password:' = text_field_tag :new_password, nil, type: 'password', autocomplete: 'off', required: 'true', class: 'form-control' -- GitLab From 80aace254d33ce4a0e3422f650684387648f71c3 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Fri, 28 Jan 2022 14:57:20 +0100 Subject: [PATCH 14/16] Add dependency on omniauth_openid_connect 0.4.0+ Signed-off-by: Andrej Shadura --- src/api/Gemfile | 1 + src/api/Gemfile.lock | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/api/Gemfile b/src/api/Gemfile index d355553048..0d0b3ff9c9 100644 --- a/src/api/Gemfile +++ b/src/api/Gemfile @@ -88,6 +88,7 @@ gem 'ajax-datatables-rails' # SSO gem 'omniauth' gem 'omniauth-gitlab' +gem 'omniauth_openid_connect', '~> 0.4.0' gem 'omniauth-rails_csrf_protection' group :development, :production do diff --git a/src/api/Gemfile.lock b/src/api/Gemfile.lock index 7b82ef3841..7142282ac7 100644 --- a/src/api/Gemfile.lock +++ b/src/api/Gemfile.lock @@ -52,6 +52,7 @@ GEM activerecord (>= 3.0.0) addressable (2.6.0) public_suffix (>= 2.0.2, < 4.0) + aes_key_wrap (1.1.0) airbrake (8.0.1) airbrake-ruby (~> 3.0) airbrake-ruby (3.1.0) @@ -62,9 +63,11 @@ GEM ansi (1.5.0) arel (9.0.0) ast (2.4.0) + attr_required (1.0.1) autoprefixer-rails (9.6.0) execjs bcrypt (3.1.13) + bindata (2.4.10) bootstrap (4.3.1) autoprefixer-rails (>= 9.1.0) popper_js (>= 1.14.3, < 2) @@ -209,6 +212,7 @@ GEM haml (>= 4.0, < 6) nokogiri (>= 1.6.0) ruby_parser (~> 3.5) + httpclient (2.8.3) i18n (1.8.11) concurrent-ruby (~> 1.0) influxdb (0.7.0) @@ -224,6 +228,10 @@ GEM jquery-ui-rails (4.2.1) railties (>= 3.2.16) json (2.5.1) + json-jwt (1.13.0) + activesupport (>= 4.2) + aes_key_wrap + bindata jwt (2.3.0) kaminari (1.2.1) activesupport (>= 4.1.0) @@ -299,6 +307,20 @@ GEM omniauth-rails_csrf_protection (1.0.0) actionpack (>= 4.2) omniauth (~> 2.0) + omniauth_openid_connect (0.4.0) + addressable (~> 2.5) + omniauth (>= 1.9, < 3) + openid_connect (~> 1.1) + openid_connect (1.3.0) + activemodel + attr_required (>= 1.0.0) + json-jwt (>= 1.5.0) + rack-oauth2 (>= 1.6.1) + swd (>= 1.0.0) + tzinfo + validate_email + validate_url + webfinger (>= 1.0.1) parallel (1.17.0) parser (2.6.3.0) ast (~> 2.4.0) @@ -337,6 +359,12 @@ GEM activesupport (>= 3.0.0) racc (1.5.2) rack (2.2.3) + rack-oauth2 (1.19.0) + activesupport + attr_required + httpclient + json-jwt (>= 1.11.0) + rack (>= 2.1.0) rack-protection (2.1.0) rack rack-test (1.1.0) @@ -470,6 +498,10 @@ GEM unicode-display_width (~> 1.5) unicode_utils (~> 1.4) strings-ansi (0.1.0) + swd (1.3.0) + activesupport (>= 3) + attr_required (>= 0.0.5) + httpclient (>= 2.4) sysexits (1.2.0) tdigest (0.1.1) rbtree (~> 0.4.2) @@ -505,9 +537,18 @@ GEM unicode-display_width (1.6.0) unicode_utils (1.4.0) uniform_notifier (1.12.1) + validate_email (0.1.6) + activemodel (>= 3.0) + mail (>= 2.2.5) + validate_url (1.0.13) + activemodel (>= 3.0.0) + public_suffix vcr (5.0.0) voight_kampff (1.1.3) rack (>= 1.4, < 3.0) + webfinger (1.2.0) + activesupport + httpclient (>= 2.4) webmock (3.6.0) addressable (>= 2.3.6) crack (>= 0.3.2) @@ -585,6 +626,7 @@ DEPENDENCIES omniauth omniauth-gitlab omniauth-rails_csrf_protection + omniauth_openid_connect peek peek-dalli peek-host -- GitLab From 1683e1707e0084fc8fe7e1becc64ee7a68400550 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Mon, 31 Jan 2022 15:13:33 +0100 Subject: [PATCH 15/16] Add omniauth-github Signed-off-by: Andrej Shadura --- src/api/Gemfile | 1 + src/api/Gemfile.lock | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/api/Gemfile b/src/api/Gemfile index 0d0b3ff9c9..ee8ce23fdd 100644 --- a/src/api/Gemfile +++ b/src/api/Gemfile @@ -88,6 +88,7 @@ gem 'ajax-datatables-rails' # SSO gem 'omniauth' gem 'omniauth-gitlab' +gem 'omniauth-github' gem 'omniauth_openid_connect', '~> 0.4.0' gem 'omniauth-rails_csrf_protection' diff --git a/src/api/Gemfile.lock b/src/api/Gemfile.lock index 7142282ac7..b460054fb3 100644 --- a/src/api/Gemfile.lock +++ b/src/api/Gemfile.lock @@ -298,6 +298,9 @@ GEM hashie (>= 3.4.6) rack (>= 1.6.2, < 3) rack-protection + omniauth-github (2.0.0) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.7.1) omniauth-gitlab (3.0.0) omniauth (~> 2.0) omniauth-oauth2 (~> 1.7.1) @@ -624,6 +627,7 @@ DEPENDENCIES mysql2 nokogiri omniauth + omniauth-github omniauth-gitlab omniauth-rails_csrf_protection omniauth_openid_connect -- GitLab From 04d85c79fb4f3b3402ef9a782f3f32cd92df84b5 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Mon, 31 Jan 2022 18:33:02 +0100 Subject: [PATCH 16/16] Only store the 'info' part of the auth hash in the session The auth hash can be quite large, and with session storage in cookies, the cookie can easily reach the 4 KB limit. Work around this issue by only storing the part of the hash we currently use. Signed-off-by: Andrej Shadura --- .../controllers/webui/session_controller.rb | 23 ++++++++++--------- src/api/app/models/user.rb | 8 +++---- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/api/app/controllers/webui/session_controller.rb b/src/api/app/controllers/webui/session_controller.rb index 29973ef7cb..e602cf7fe0 100644 --- a/src/api/app/controllers/webui/session_controller.rb +++ b/src/api/app/controllers/webui/session_controller.rb @@ -46,10 +46,11 @@ class Webui::SessionController < Webui::WebuiController def sso_callback @auth_hash = request.env['omniauth.auth'] - user = User.find_with_omniauth(@auth_hash) + user = User.find_with_omniauth(@auth_hash['info']) unless user - session[:auth] = @auth_hash + session[:auth] = @auth_hash['info'] + session[:auth]['provider'] = @auth_hash['provider'] redirect_to(sso_confirm_path) return end @@ -69,19 +70,19 @@ class Webui::SessionController < Webui::WebuiController def sso_confirm switch_to_webui2 - auth_hash = session[:auth] + auth_info = session[:auth] - if !auth_hash + if !auth_info redirect_to sso_path return end # Try to derive a username from the information available, # falling back to full name if nothing else works - @derived_username = auth_hash['info']['username'] || - auth_hash['info']['nickname'] || - auth_hash['info']['email'] || - auth_hash['info']['name'] + @derived_username = auth_info['username'] || + auth_info['nickname'] || + auth_info['email'] || + auth_info['name'] # Some providers set username or nickname to an email address # Derive the username from the local part of the email address, @@ -93,9 +94,9 @@ class Webui::SessionController < Webui::WebuiController def do_sso_confirm required_parameters :login - auth_hash = session[:auth] + auth_info = session[:auth] - if !auth_hash + if !auth_info redirect_to sso_path return end @@ -108,7 +109,7 @@ class Webui::SessionController < Webui::WebuiController end begin - user = User.create_with_omniauth(auth_hash, params[:login]) + user = User.create_with_omniauth(auth_info, params[:login]) rescue ActiveRecord::ActiveRecordError flash[:error] = "Invalid username, please try a different one" redirect_to sso_confirm_path diff --git a/src/api/app/models/user.rb b/src/api/app/models/user.rb index fd21734188..1156e291a7 100644 --- a/src/api/app/models/user.rb +++ b/src/api/app/models/user.rb @@ -226,7 +226,7 @@ class User < ApplicationRecord def self.find_with_omniauth(auth) if auth - email = auth['info']['email'] + email = auth['email'] user = find_by_email(email) if user user.mark_login! @@ -238,14 +238,14 @@ class User < ApplicationRecord def self.create_with_omniauth(auth, login) provider = CONFIG['sso_auth'][auth['provider']]['description'] - email = auth['info']['email'] + email = auth['email'] logger.debug("Creating OmniAuth user for #{provider}") logger.debug("Email: #{email}") - logger.debug("Name : #{auth['info']['name']}") + logger.debug("Name : #{auth['name']}") user = create_external_user(login: login, email: email, - realname: auth['info']['name'], + realname: auth['name'], deprecated_password_hash_type: 'invalid', adminnote: "User created via #{provider}") user.mark_login! -- GitLab