diff --git a/.github/workflows/docker-gar.yml b/.github/workflows/docker-gar.yml new file mode 100644 index 000000000..506e33a93 --- /dev/null +++ b/.github/workflows/docker-gar.yml @@ -0,0 +1,71 @@ +name: Build and Push to Google Artifact Registry + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: read + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Create .version file + env: + REF_TYPE: ${{ github.ref_type }} + REF_NAME: ${{ github.ref_name }} + COMMIT_SHA: ${{ github.sha }} + run: | + if [[ "${REF_TYPE}" == "tag" ]]; then + echo "${REF_NAME}" > .version + else + echo "${COMMIT_SHA}" > .version + fi + + - name: Authenticate to Google Cloud + id: auth + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + token_format: access_token + + - name: Login to Artifact Registry + uses: docker/login-action@v3 + with: + registry: us-central1-docker.pkg.dev + username: oauth2accesstoken + password: ${{ steps.auth.outputs.access_token }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: us-central1-docker.pkg.dev/kencove-prod/docuseal/docuseal + tags: | + type=semver,pattern={{version}} + type=sha,prefix=,format=short + type=raw,value=latest,enable={{is_default_branch}} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64 + tags: ${{ steps.meta.outputs.tags }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Gemfile b/Gemfile index 61c848072..416dca88d 100644 --- a/Gemfile +++ b/Gemfile @@ -25,6 +25,8 @@ gem 'jwt', require: false gem 'lograge' gem 'numo-narray-alt', require: false gem 'oj' +gem 'omniauth-google-oauth2' +gem 'omniauth-rails_csrf_protection' gem 'onnxruntime', require: false gem 'pagy' gem 'pg', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 962c2364a..4f9e416bc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -268,6 +268,8 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) hashdiff (1.2.1) + hashie (5.1.0) + logger hexapdf (1.5.0) cmdparse (~> 3.0, >= 3.0.3) geom2d (~> 0.4, >= 0.4.1) @@ -326,6 +328,8 @@ GEM prism (~> 1.5) msgpack (1.8.0) multi_json (1.19.1) + multi_xml (0.8.1) + bigdecimal (>= 3.1, < 5) net-http (0.9.1) uri (>= 0.11.1) net-imap (0.6.2) @@ -349,9 +353,33 @@ GEM nokogiri (1.19.0-x86_64-linux-musl) racc (~> 1.4) numo-narray-alt (0.9.13) + oauth2 (2.0.18) + faraday (>= 0.17.3, < 4.0) + jwt (>= 1.0, < 4.0) + logger (~> 1.2) + multi_xml (~> 0.5) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0, >= 2.0.3) + version_gem (~> 1.1, >= 1.1.9) oj (3.16.13) bigdecimal (>= 3.0) ostruct (>= 0.2) + omniauth (2.1.4) + hashie (>= 3.4.6) + logger + rack (>= 2.2.3) + rack-protection + omniauth-google-oauth2 (1.2.1) + jwt (>= 2.9.2) + oauth2 (~> 2.0) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.8) + omniauth-oauth2 (1.9.0) + oauth2 (>= 2.0.2, < 3) + omniauth (~> 2.0) + omniauth-rails_csrf_protection (2.0.1) + actionpack (>= 4.2) + omniauth (~> 2.0) onnxruntime (0.10.1) ffi onnxruntime (0.10.1-aarch64-linux) @@ -406,6 +434,10 @@ GEM nio4r (~> 2.0) racc (1.8.1) rack (3.2.4) + rack-protection (4.2.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) rack-proxy (0.7.7) rack rack-session (2.1.1) @@ -558,6 +590,9 @@ GEM simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) smart_properties (1.17.0) + snaky_hash (2.0.3) + hashie (>= 0.1.0, < 6) + version_gem (>= 1.1.8, < 3) sqlite3 (2.9.0-aarch64-linux-gnu) sqlite3 (2.9.0-aarch64-linux-musl) sqlite3 (2.9.0-arm64-darwin) @@ -590,6 +625,7 @@ GEM uniform_notifier (1.18.0) uri (1.1.1) useragent (0.16.11) + version_gem (1.1.9) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.1) @@ -652,6 +688,8 @@ DEPENDENCIES lograge numo-narray-alt oj + omniauth-google-oauth2 + omniauth-rails_csrf_protection onnxruntime pagy pg diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..26a625a3e --- /dev/null +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class OmniauthCallbacksController < Devise::OmniauthCallbacksController + skip_before_action :verify_authenticity_token, only: :google_oauth2 + + def google_oauth2 + user = Users.from_omniauth(request.env['omniauth.auth']) + + if user&.active_for_authentication? + sign_in_and_redirect(user, event: :authentication) + else + redirect_to new_user_session_path, alert: I18n.t('user_not_found') + end + end + + def failure + redirect_to new_user_session_path, alert: I18n.t('authentication_failed') + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 7eabb0592..3ca40d563 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -69,6 +69,7 @@ class User < ApplicationRecord has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable, :lockable + devise :omniauthable, omniauth_providers: [:google_oauth2] if ENV['GOOGLE_CLIENT_ID'].present? attribute :role, :string, default: ADMIN_ROLE attribute :uuid, :string, default: -> { SecureRandom.uuid } diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index efa97448c..19c8d9d70 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -334,6 +334,13 @@ def devise_mail(record, action, opts = {}, &) # changed. Defaults to true, so a user is signed in automatically after changing a password. # config.sign_in_after_change_password = true + if ENV['GOOGLE_CLIENT_ID'].present? + config.omniauth :google_oauth2, + ENV.fetch('GOOGLE_CLIENT_ID'), + ENV.fetch('GOOGLE_CLIENT_SECRET'), + { hd: ENV.fetch('GOOGLE_HOSTED_DOMAIN', 'kencove.com') } + end + ActiveSupport.run_load_hooks(:devise_config, config) end # rubocop:enable Metrics/BlockLength diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index c95a793a0..02de4c5b4 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -173,6 +173,8 @@ en: &en sign_in: Sign In signing_in: Signing In sign_in_with_microsoft: Sign in with Microsoft + user_not_found: User not found. Please contact your administrator. + authentication_failed: Authentication failed. Please try again. sign_in_with_google: Sign in with Google forgot_your_password_: Forgot your password? create_free_account: Create free account diff --git a/config/routes.rb b/config/routes.rb index 4447a2399..2f88937fb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,8 +14,15 @@ get 'up' => 'rails/health#show' get 'manifest' => 'pwa#manifest' - devise_for :users, path: '/', only: %i[sessions passwords], - controllers: { sessions: 'sessions', passwords: 'passwords' } + devise_actions = %i[sessions passwords] + devise_controllers = { sessions: 'sessions', passwords: 'passwords' } + + if ENV['GOOGLE_CLIENT_ID'].present? + devise_actions << :omniauth_callbacks + devise_controllers[:omniauth_callbacks] = 'omniauth_callbacks' + end + + devise_for :users, path: '/', only: devise_actions, controllers: devise_controllers devise_scope :user do resource :invitation, only: %i[update] do diff --git a/lib/users.rb b/lib/users.rb new file mode 100644 index 000000000..f73b77fd6 --- /dev/null +++ b/lib/users.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Users + module_function + + def from_omniauth(oauth) + User.find_by(email: oauth.info.email.to_s.downcase) + end +end