From 7adc95485354bef2d8a0e320b1b0fcfb7ce474d4 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 25 Jun 2021 22:08:36 +0300 Subject: [PATCH 01/65] Initial commit --- .gitattributes | 10 + .gitignore | 44 +++ .ruby-version | 1 + Dockerfile | 17 + Gemfile | 37 +++ Gemfile.lock | 303 ++++++++++++++++++ README.md | 2 +- Rakefile | 6 + app/controllers/application_controller.rb | 11 + app/controllers/concerns/.keep | 0 app/controllers/home_controller.rb | 8 + app/controllers/v1/branches_controller.rb | 25 ++ app/controllers/v1/repositories_controller.rb | 20 ++ .../v1/server_providers_controller.rb | 116 +++++++ .../v1/users/confirmations_controller.rb | 25 ++ .../v1/users/registrations_controller.rb | 33 ++ .../v1/users/sessions_controller.rb | 25 ++ app/controllers/v1/users_controller.rb | 76 +++++ app/helpers/application_helper.rb | 2 + app/javascript/packs/application.js | 8 + app/javascript/src/adapters/.gitkeep | 0 app/javascript/src/adapters/application.js | 4 + app/javascript/src/app.js | 11 + app/javascript/src/application.js | 12 + app/javascript/src/components/.gitkeep | 0 app/javascript/src/controllers/.gitkeep | 0 app/javascript/src/environment.js | 7 + app/javascript/src/helpers/.gitkeep | 0 app/javascript/src/initializers/.gitkeep | 0 .../src/instance-initializers/.gitkeep | 0 app/javascript/src/mixins/.gitkeep | 0 app/javascript/src/models/.gitkeep | 0 app/javascript/src/router.js | 5 + app/javascript/src/routes/.gitkeep | 0 app/javascript/src/serializers/.gitkeep | 0 app/javascript/src/services/.gitkeep | 0 app/javascript/src/templates/.gitkeep | 0 .../src/templates/components/.gitkeep | 0 app/javascript/src/transforms/.gitkeep | 0 app/javascript/src/views/.gitkeep | 0 app/jobs/application_job.rb | 7 + app/jobs/sync_job.rb | 20 ++ app/mailers/application_mailer.rb | 4 + app/models/application_record.rb | 3 + app/models/concerns/.keep | 0 app/models/p4_server_provider.rb | 5 + app/models/p4_server_setting.rb | 7 + app/models/ref.rb | 15 + app/models/repository.rb | 18 ++ app/models/repository_permission.rb | 11 + app/models/server_provider.rb | 10 + app/models/server_provider_permission.rb | 15 + app/models/server_provider_user_setting.rb | 51 +++ app/models/user.rb | 45 +++ app/serializers/application_serializer.rb | 19 ++ app/serializers/full_ref_serializer.rb | 7 + app/serializers/ref_serializer.rb | 5 + app/serializers/repository_serializer.rb | 5 + app/serializers/server_provider_serializer.rb | 5 + app/serializers/user_serializer.rb | 6 + .../mailer/confirmation_instructions.html.erb | 5 + .../reset_password_instructions.html.erb | 8 + .../devise/shared/_error_messages.html.erb | 15 + app/views/devise/shared/_links.html.erb | 25 ++ bin/bundle | 114 +++++++ bin/rails | 5 + bin/rake | 5 + bin/setup | 36 +++ bin/spring | 14 + bin/webpack | 18 ++ bin/webpack-dev-server | 18 ++ bin/yarn | 17 + config.ru | 6 + config/application.rb | 29 ++ config/boot.rb | 4 + config/cable.yml | 10 + config/credentials.yml.enc | 1 + config/database.yml | 19 ++ config/environment.rb | 5 + config/environments/development.rb | 75 +++++ config/environments/production.rb | 104 ++++++ config/environments/test.rb | 57 ++++ .../application_controller_renderer.rb | 8 + config/initializers/backtrace_silencers.rb | 8 + config/initializers/config.rb | 58 ++++ .../initializers/content_security_policy.rb | 30 ++ config/initializers/cookies_serializer.rb | 5 + config/initializers/cors.rb | 10 + config/initializers/devise.rb | 289 +++++++++++++++++ .../initializers/filter_parameter_logging.rb | 6 + config/initializers/inflections.rb | 16 + config/initializers/mime_types.rb | 4 + config/initializers/permissions_policy.rb | 11 + config/initializers/wrap_parameters.rb | 14 + config/locales/devise.en.yml | 65 ++++ config/locales/en.yml | 33 ++ config/puma.rb | 43 +++ config/routes.rb | 47 +++ config/settings.yml | 5 + config/settings/development.yml | 0 config/settings/production.yml | 0 config/settings/test.yml | 0 config/spring.rb | 6 + config/webpack/development.js | 5 + config/webpack/environment.js | 3 + config/webpack/production.js | 5 + config/webpack/test.js | 5 + config/webpacker.yml | 92 ++++++ db/schema.rb | 134 ++++++++ db/seeds.rb | 7 + entrypoint.sh | 5 + lib/assets/.keep | 0 lib/tasks/.keep | 0 lib/travis/vcs_proxy/p4_connection.rb | 67 ++++ lib/travis/vcs_proxy/sync/repository.rb | 16 + lib/travis/vcs_proxy/sync/server_provider.rb | 36 +++ lib/travis/vcs_proxy/sync/user.rb | 16 + lib/travis/vcs_proxy/syncer.rb | 19 ++ log/.keep | 0 public/404.html | 67 ++++ public/422.html | 67 ++++ public/500.html | 66 ++++ public/apple-touch-icon-precomposed.png | 0 public/apple-touch-icon.png | 0 public/favicon.ico | 0 public/robots.txt | 1 + spec/jobs/sync_job_spec.rb | 5 + spec/models/jwt_deny_list_spec.rb | 5 + spec/models/user_spec.rb | 5 + tmp/.keep | 0 tmp/pids/.keep | 0 vendor/.keep | 0 132 files changed, 2939 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .ruby-version create mode 100644 Dockerfile create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 Rakefile create mode 100644 app/controllers/application_controller.rb create mode 100644 app/controllers/concerns/.keep create mode 100644 app/controllers/home_controller.rb create mode 100644 app/controllers/v1/branches_controller.rb create mode 100644 app/controllers/v1/repositories_controller.rb create mode 100644 app/controllers/v1/server_providers_controller.rb create mode 100644 app/controllers/v1/users/confirmations_controller.rb create mode 100644 app/controllers/v1/users/registrations_controller.rb create mode 100644 app/controllers/v1/users/sessions_controller.rb create mode 100644 app/controllers/v1/users_controller.rb create mode 100644 app/helpers/application_helper.rb create mode 100644 app/javascript/packs/application.js create mode 100644 app/javascript/src/adapters/.gitkeep create mode 100644 app/javascript/src/adapters/application.js create mode 100644 app/javascript/src/app.js create mode 100644 app/javascript/src/application.js create mode 100644 app/javascript/src/components/.gitkeep create mode 100644 app/javascript/src/controllers/.gitkeep create mode 100644 app/javascript/src/environment.js create mode 100644 app/javascript/src/helpers/.gitkeep create mode 100644 app/javascript/src/initializers/.gitkeep create mode 100644 app/javascript/src/instance-initializers/.gitkeep create mode 100644 app/javascript/src/mixins/.gitkeep create mode 100644 app/javascript/src/models/.gitkeep create mode 100644 app/javascript/src/router.js create mode 100644 app/javascript/src/routes/.gitkeep create mode 100644 app/javascript/src/serializers/.gitkeep create mode 100644 app/javascript/src/services/.gitkeep create mode 100644 app/javascript/src/templates/.gitkeep create mode 100644 app/javascript/src/templates/components/.gitkeep create mode 100644 app/javascript/src/transforms/.gitkeep create mode 100644 app/javascript/src/views/.gitkeep create mode 100644 app/jobs/application_job.rb create mode 100644 app/jobs/sync_job.rb create mode 100644 app/mailers/application_mailer.rb create mode 100644 app/models/application_record.rb create mode 100644 app/models/concerns/.keep create mode 100644 app/models/p4_server_provider.rb create mode 100644 app/models/p4_server_setting.rb create mode 100644 app/models/ref.rb create mode 100644 app/models/repository.rb create mode 100644 app/models/repository_permission.rb create mode 100644 app/models/server_provider.rb create mode 100644 app/models/server_provider_permission.rb create mode 100644 app/models/server_provider_user_setting.rb create mode 100644 app/models/user.rb create mode 100644 app/serializers/application_serializer.rb create mode 100644 app/serializers/full_ref_serializer.rb create mode 100644 app/serializers/ref_serializer.rb create mode 100644 app/serializers/repository_serializer.rb create mode 100644 app/serializers/server_provider_serializer.rb create mode 100644 app/serializers/user_serializer.rb create mode 100644 app/views/devise/mailer/confirmation_instructions.html.erb create mode 100644 app/views/devise/mailer/reset_password_instructions.html.erb create mode 100644 app/views/devise/shared/_error_messages.html.erb create mode 100644 app/views/devise/shared/_links.html.erb create mode 100755 bin/bundle create mode 100755 bin/rails create mode 100755 bin/rake create mode 100755 bin/setup create mode 100755 bin/spring create mode 100755 bin/webpack create mode 100755 bin/webpack-dev-server create mode 100755 bin/yarn create mode 100644 config.ru create mode 100644 config/application.rb create mode 100644 config/boot.rb create mode 100644 config/cable.yml create mode 100644 config/credentials.yml.enc create mode 100644 config/database.yml create mode 100644 config/environment.rb create mode 100644 config/environments/development.rb create mode 100644 config/environments/production.rb create mode 100644 config/environments/test.rb create mode 100644 config/initializers/application_controller_renderer.rb create mode 100644 config/initializers/backtrace_silencers.rb create mode 100644 config/initializers/config.rb create mode 100644 config/initializers/content_security_policy.rb create mode 100644 config/initializers/cookies_serializer.rb create mode 100644 config/initializers/cors.rb create mode 100644 config/initializers/devise.rb create mode 100644 config/initializers/filter_parameter_logging.rb create mode 100644 config/initializers/inflections.rb create mode 100644 config/initializers/mime_types.rb create mode 100644 config/initializers/permissions_policy.rb create mode 100644 config/initializers/wrap_parameters.rb create mode 100644 config/locales/devise.en.yml create mode 100644 config/locales/en.yml create mode 100644 config/puma.rb create mode 100644 config/routes.rb create mode 100644 config/settings.yml create mode 100644 config/settings/development.yml create mode 100644 config/settings/production.yml create mode 100644 config/settings/test.yml create mode 100644 config/spring.rb create mode 100644 config/webpack/development.js create mode 100644 config/webpack/environment.js create mode 100644 config/webpack/production.js create mode 100644 config/webpack/test.js create mode 100644 config/webpacker.yml create mode 100644 db/schema.rb create mode 100644 db/seeds.rb create mode 100755 entrypoint.sh create mode 100644 lib/assets/.keep create mode 100644 lib/tasks/.keep create mode 100644 lib/travis/vcs_proxy/p4_connection.rb create mode 100644 lib/travis/vcs_proxy/sync/repository.rb create mode 100644 lib/travis/vcs_proxy/sync/server_provider.rb create mode 100644 lib/travis/vcs_proxy/sync/user.rb create mode 100644 lib/travis/vcs_proxy/syncer.rb create mode 100644 log/.keep create mode 100644 public/404.html create mode 100644 public/422.html create mode 100644 public/500.html create mode 100644 public/apple-touch-icon-precomposed.png create mode 100644 public/apple-touch-icon.png create mode 100644 public/favicon.ico create mode 100644 public/robots.txt create mode 100644 spec/jobs/sync_job_spec.rb create mode 100644 spec/models/jwt_deny_list_spec.rb create mode 100644 spec/models/user_spec.rb create mode 100644 tmp/.keep create mode 100644 tmp/pids/.keep create mode 100644 vendor/.keep diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..51685710 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark the yarn lockfile as having been generated. +yarn.lock linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..dc5db11b --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-* + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore uploaded files in development. +/storage/* +!/storage/.keep + +/public/assets +.byebug_history + +# Ignore master key for decrypting credentials and more. +/config/master.key + +/public/packs +/public/packs-test +/node_modules +/yarn-error.log +yarn-debug.log* +.yarn-integrity + +config/settings.local.yml +config/settings/*.local.yml +config/environments/*.local.yml diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..2eb2fe97 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-2.7.2 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..3488405f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM ruby:3.0.1 + +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ + curl -sL https://deb.nodesource.com/setup_16.x | bash -s && \ + apt-get install -y --no-install-recommends \ + postgresql-client nodejs yarn \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /srv/app +# COPY Gemfile* ./ +# RUN bundle install +# COPY . . + +EXPOSE 3000 +ENTRYPOINT ["/srv/app/entrypoint.sh"] +CMD ["rails", "server", "-b", "0.0.0.0"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..f5456542 --- /dev/null +++ b/Gemfile @@ -0,0 +1,37 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +ruby '3.0.1' + +gem 'rails', '~> 6.1.3', '>= 6.1.3.2' +gem 'devise' +gem 'devise-jwt' +gem 'devise-two-factor' +gem 'ledermann-rails-settings' +gem 'pg' +gem 'redis' +gem 'puma', '~> 5.0' +gem 'rack-cors' +gem 'config' +gem 'jsonapi-serializer' +gem 'p4ruby' +gem 'sidekiq' + +gem 'bootsnap', require: false + +group :development, :test do + gem 'brakeman' + gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + gem 'factory_bot' + gem 'rspec-rails' + gem 'listen' +end + +group :test do + gem 'rspec' +end + +group :development do + gem 'rubocop', '~> 0.75.1', require: false + gem 'rubocop-rspec' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..f2869fe9 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,303 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (6.1.3.2) + actionpack (= 6.1.3.2) + activesupport (= 6.1.3.2) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (6.1.3.2) + actionpack (= 6.1.3.2) + activejob (= 6.1.3.2) + activerecord (= 6.1.3.2) + activestorage (= 6.1.3.2) + activesupport (= 6.1.3.2) + mail (>= 2.7.1) + actionmailer (6.1.3.2) + actionpack (= 6.1.3.2) + actionview (= 6.1.3.2) + activejob (= 6.1.3.2) + activesupport (= 6.1.3.2) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (6.1.3.2) + actionview (= 6.1.3.2) + activesupport (= 6.1.3.2) + rack (~> 2.0, >= 2.0.9) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.1.3.2) + actionpack (= 6.1.3.2) + activerecord (= 6.1.3.2) + activestorage (= 6.1.3.2) + activesupport (= 6.1.3.2) + nokogiri (>= 1.8.5) + actionview (6.1.3.2) + activesupport (= 6.1.3.2) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (6.1.3.2) + activesupport (= 6.1.3.2) + globalid (>= 0.3.6) + activemodel (6.1.3.2) + activesupport (= 6.1.3.2) + activerecord (6.1.3.2) + activemodel (= 6.1.3.2) + activesupport (= 6.1.3.2) + activestorage (6.1.3.2) + actionpack (= 6.1.3.2) + activejob (= 6.1.3.2) + activerecord (= 6.1.3.2) + activesupport (= 6.1.3.2) + marcel (~> 1.0.0) + mini_mime (~> 1.0.2) + activesupport (6.1.3.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + ast (2.4.2) + attr_encrypted (3.1.0) + encryptor (~> 3.0.0) + bcrypt (3.1.16) + bootsnap (1.7.5) + msgpack (~> 1.0) + brakeman (5.0.4) + builder (3.2.4) + byebug (11.1.3) + concurrent-ruby (1.1.9) + config (3.1.0) + deep_merge (~> 1.2, >= 1.2.1) + dry-validation (~> 1.0, >= 1.0.0) + connection_pool (2.2.5) + crass (1.0.6) + deep_merge (1.2.1) + devise (4.8.0) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + devise-jwt (0.8.1) + devise (~> 4.0) + warden-jwt_auth (~> 0.5) + devise-two-factor (4.0.0) + activesupport (< 6.2) + attr_encrypted (>= 1.3, < 4, != 2) + devise (~> 4.0) + railties (< 6.2) + rotp (~> 6.0) + diff-lcs (1.4.4) + dry-auto_inject (0.8.0) + dry-container (>= 0.3.4) + dry-configurable (0.12.1) + concurrent-ruby (~> 1.0) + dry-core (~> 0.5, >= 0.5.0) + dry-container (0.8.0) + concurrent-ruby (~> 1.0) + dry-configurable (~> 0.1, >= 0.1.3) + dry-core (0.6.0) + concurrent-ruby (~> 1.0) + dry-equalizer (0.3.0) + dry-inflector (0.2.0) + dry-initializer (3.0.4) + dry-logic (1.2.0) + concurrent-ruby (~> 1.0) + dry-core (~> 0.5, >= 0.5) + dry-schema (1.6.2) + concurrent-ruby (~> 1.0) + dry-configurable (~> 0.8, >= 0.8.3) + dry-core (~> 0.5, >= 0.5) + dry-initializer (~> 3.0) + dry-logic (~> 1.0) + dry-types (~> 1.5) + dry-types (1.5.1) + concurrent-ruby (~> 1.0) + dry-container (~> 0.3) + dry-core (~> 0.5, >= 0.5) + dry-inflector (~> 0.1, >= 0.1.2) + dry-logic (~> 1.0, >= 1.0.2) + dry-validation (1.6.0) + concurrent-ruby (~> 1.0) + dry-container (~> 0.7, >= 0.7.1) + dry-core (~> 0.4) + dry-equalizer (~> 0.2) + dry-initializer (~> 3.0) + dry-schema (~> 1.5, >= 1.5.2) + encryptor (3.0.0) + erubi (1.10.0) + factory_bot (6.2.0) + activesupport (>= 5.0.0) + ffi (1.15.1) + globalid (0.4.2) + activesupport (>= 4.2.0) + i18n (1.8.10) + concurrent-ruby (~> 1.0) + jaro_winkler (1.5.4) + jsonapi-serializer (2.2.0) + activesupport (>= 4.2) + jwt (2.2.3) + ledermann-rails-settings (2.5.0) + activerecord (>= 4.2) + listen (3.5.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + loofah (2.10.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (1.0.1) + method_source (1.0.0) + mini_mime (1.0.3) + mini_portile2 (2.5.3) + minitest (5.14.4) + msgpack (1.4.2) + nio4r (2.5.7) + nokogiri (1.11.7) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) + orm_adapter (0.5.0) + p4ruby (2020.1.2056123) + parallel (1.20.1) + parser (3.0.1.1) + ast (~> 2.4.1) + pg (1.2.3) + puma (5.3.2) + nio4r (~> 2.0) + racc (1.5.2) + rack (2.2.3) + rack-cors (1.1.1) + rack (>= 2.0.0) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (6.1.3.2) + actioncable (= 6.1.3.2) + actionmailbox (= 6.1.3.2) + actionmailer (= 6.1.3.2) + actionpack (= 6.1.3.2) + actiontext (= 6.1.3.2) + actionview (= 6.1.3.2) + activejob (= 6.1.3.2) + activemodel (= 6.1.3.2) + activerecord (= 6.1.3.2) + activestorage (= 6.1.3.2) + activesupport (= 6.1.3.2) + bundler (>= 1.15.0) + railties (= 6.1.3.2) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.3.0) + loofah (~> 2.3) + railties (6.1.3.2) + actionpack (= 6.1.3.2) + activesupport (= 6.1.3.2) + method_source + rake (>= 0.8.7) + thor (~> 1.0) + rainbow (3.0.0) + rake (13.0.3) + rb-fsevent (0.11.0) + rb-inotify (0.10.1) + ffi (~> 1.0) + redis (4.3.1) + responders (3.0.1) + actionpack (>= 5.0) + railties (>= 5.0) + rotp (6.2.0) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-rails (5.0.1) + actionpack (>= 5.2) + activesupport (>= 5.2) + railties (>= 5.2) + rspec-core (~> 3.10) + rspec-expectations (~> 3.10) + rspec-mocks (~> 3.10) + rspec-support (~> 3.10) + rspec-support (3.10.2) + rubocop (0.75.1) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.6) + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 1.7) + rubocop-rspec (1.41.0) + rubocop (>= 0.68.1) + ruby-progressbar (1.11.0) + sidekiq (6.2.1) + connection_pool (>= 2.2.2) + rack (~> 2.0) + redis (>= 4.2.0) + sprockets (4.0.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.2) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + thor (1.1.0) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + unicode-display_width (1.6.1) + warden (1.2.9) + rack (>= 2.0.9) + warden-jwt_auth (0.5.0) + dry-auto_inject (~> 0.6) + dry-configurable (~> 0.9) + jwt (~> 2.1) + warden (~> 1.2) + websocket-driver (0.7.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.4.2) + +PLATFORMS + ruby + +DEPENDENCIES + bootsnap + brakeman + byebug + config + devise + devise-jwt + devise-two-factor + factory_bot + jsonapi-serializer + ledermann-rails-settings + listen + p4ruby + pg + puma (~> 5.0) + rack-cors + rails (~> 6.1.3, >= 6.1.3.2) + redis + rspec + rspec-rails + rubocop (~> 0.75.1) + rubocop-rspec + sidekiq + +RUBY VERSION + ruby 3.0.1p64 + +BUNDLED WITH + 2.2.15 diff --git a/README.md b/README.md index 6a524a0f..1760ea77 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # travis-vcs-proxy -A Travis CI Proxy backend for integration wit VCS other than Git +A Travis CI Proxy backend for integration with VCS other than Git diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..9a5ea738 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 00000000..e23bde9f --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,11 @@ +class ApplicationController < ActionController::API + private + + def require_authentication + head :unauthorized and return unless user_signed_in? + end + + def presented_entity(resource_name, resource) + "#{resource_name.to_s.classify}Serializer".constantize.new(resource).to_h + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 00000000..0b01f0ac --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class HomeController < ApplicationController + def index + head :ok + end +end + diff --git a/app/controllers/v1/branches_controller.rb b/app/controllers/v1/branches_controller.rb new file mode 100644 index 00000000..8d57705c --- /dev/null +++ b/app/controllers/v1/branches_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class V1::BranchesController < ApplicationController + before_action :require_authentication + before_action :set_repository + before_action :set_branch, only: [:show] + + def index + render json: @repository.branches.map { |branch| presented_entity(:ref, branch) } + end + + def show + render json: presented_entity(:ref, @branch) + end + + private + + def set_repository + @repository = current_user.repositories.find(params[:repository_id]) + end + + def set_branch + @branch = @repository.branches.find(params[:id]) + end +end diff --git a/app/controllers/v1/repositories_controller.rb b/app/controllers/v1/repositories_controller.rb new file mode 100644 index 00000000..91a2b148 --- /dev/null +++ b/app/controllers/v1/repositories_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class V1::RepositoriesController < ApplicationController + before_action :require_authentication + before_action :set_repository, only: [:show, :refs] + + def show + render json: presented_entity(:repository, @repository) + end + + def refs + render json: @repository.refs.map { |ref| presented_entity(:full_ref, ref) } + end + + private + + def set_repository + @repository = current_user.repositories.find(params[:id]) + end +end diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb new file mode 100644 index 00000000..9213c7fd --- /dev/null +++ b/app/controllers/v1/server_providers_controller.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +class V1::ServerProvidersController < ApplicationController + PROVIDER_KLASS = { + 'perforce' => P4ServerProvider + }.freeze + + before_action :require_authentication + before_action :set_server_provider, only: [:show, :update, :authenticate, :forget, :repositories, :sync] + + def create + head :bad_request and return if params[:server_provider].blank? || !PROVIDER_KLASS.has_key?(params[:server_provider][:type]) + + success = true + klass = PROVIDER_KLASS[params[:server_provider][:type]] + errors = [] + ActiveRecord::Base.transaction do + unless provider = klass.find_by(name: params[:server_provider][:name]) + provider = klass.new(server_provider_params) + unless provider.save + success = false + errors = provider.errors + raise ActiveRecord::Rollback + end + end + + unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission::OWNER) + success = false + errors << 'Cannot set permission for user' + raise ActiveRecord::Rollback + end + end + + head :ok and return if success + + render json: { errors: provider.errors }, status: :unprocessable_entity + end + + def show + data = presented_entity(:server_provider, @server_provider) + data[:type] = PROVIDER_KLASS.invert[data[:type].constantize] + + render json: data + end + + def update + permission = current_user.server_provider_permission(@server_provider.id) + head :forbidden and return if permission.blank? || !permission.owner? + + update_params = server_provider_params.dup + if params.has_key?(:server_provider) && params[:server_provider][:token].present? + update_params[:listener_token] = params[:server_provider][:token] + end + + if @server_provider.update(update_params) + head :ok + return + end + + render json: { errors: @server_provider.errors }, status: :unprocessable_entity + end + + def authenticate + head :bad_request and return if params[:token].blank? || params[:username].blank? + + success = true + ActiveRecord::Base.transaction do + permission = current_user.server_provider_permissions.first_or_initialize(server_provider_id: @server_provider.id) + unless permission.persisted? + permission.permission = ServerProviderPermission::MEMBER + unless permission.save + success = false + raise ActiveRecord::Rollback + end + end + + setting = permission.setting || permission.build_setting + setting.token = params[:token] + setting.username = params[:username] + unless setting.save + success = false + raise ActiveRecord::Rollback + end + end + + head :ok and return if success + + render json: { errors: [ 'Cannot authenticate' ] }, status: :unprocessable_entity + end + + def forget + current_user.server_provider_permission(@server_provider.id)&.destroy + + head :ok + end + + def sync + SyncJob.perform_later(SyncJob::SyncType::SERVER_PROVIDER, @server_provider.id) + + head :ok + end + + def repositories + render json: @server_provider.repositories.map { |repository| presented_entity(:repository, repository) } + end + + private + + def server_provider_params + params.require(:server_provider).permit(:name, :url) + end + + def set_server_provider + @server_provider = ServerProvider.find(params[:id]) + end +end diff --git a/app/controllers/v1/users/confirmations_controller.rb b/app/controllers/v1/users/confirmations_controller.rb new file mode 100644 index 00000000..4cbc754e --- /dev/null +++ b/app/controllers/v1/users/confirmations_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class V1::Users::ConfirmationsController < Devise::ConfirmationsController + clear_respond_to + respond_to :json + + def create + resource = nil + ActiveRecord::Base.transaction do + resource = resource_class.confirm_by_token(params[:confirmation_token]) + + if resource.errors.empty? + resource.otp_required_for_login = true + resource.otp_secret = resource_class.generate_otp_secret + raise ActiveRecord::Rollback unless resource.save + else + raise ActiveRecord::Rollback + end + end + + head :ok and return if resource.errors.empty? + + render json: { errors: resource.errors }, status: :unprocessable_entity + end +end diff --git a/app/controllers/v1/users/registrations_controller.rb b/app/controllers/v1/users/registrations_controller.rb new file mode 100644 index 00000000..1bb8a7f3 --- /dev/null +++ b/app/controllers/v1/users/registrations_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class V1::Users::RegistrationsController < Devise::RegistrationsController + clear_respond_to + respond_to :json + + def create + build_resource(sign_up_params) + + resource.save + if resource.persisted? + expire_data_after_sign_in! + head :ok + return + end + + render json: { errors: resource.errors }, status: :unprocessable_entity + end + + def update + head :not_found + end + + def destroy + resource.mark_as_deleted + sign_out + head :ok + end + + def cancel + head :not_found + end +end diff --git a/app/controllers/v1/users/sessions_controller.rb b/app/controllers/v1/users/sessions_controller.rb new file mode 100644 index 00000000..b22d0a58 --- /dev/null +++ b/app/controllers/v1/users/sessions_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class V1::Users::SessionsController < Devise::SessionsController + clear_respond_to + respond_to :json + + # Overriding because we don't need to send any answer + def create + resource = warden.authenticate!(auth_options) + sign_in(resource_name, resource) + + head :ok + end + + private + + # Overriding because we don't need to send any answer + def respond_to_on_destroy + head :ok + end + + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt]) + end +end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb new file mode 100644 index 00000000..604d92b3 --- /dev/null +++ b/app/controllers/v1/users_controller.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +class V1::UsersController < ApplicationController + before_action :require_authentication, except: [:request_password_reset, :reset_password] + + def show + render json: presented_entity(:user, current_user) + end + + def update_email + head :bad_request and return if params[:email].blank? + + current_user.email = params[:email] + if current_user.save + head :ok + return + end + + render json: { errors: current_user.errors }, status: :unprocessable_entity + end + + def update_password + head :bad_request and return if params[:password].blank? || params[:password_confirmation].blank? + + unless params[:password] == params[:password_confirmation] + render json: { errors: [ 'Password does not match confirmation' ] }, status: :unprocessable_entity + return + end + + current_user.password = params[:password] + current_user.password_confirmation = params[:password_confirmation] + if current_user.save + head :ok + return + end + + render json: { errors: current_user.errors }, status: :unprocessable_entity + end + + def request_password_reset + head :ok and return if params[:email].blank? + head :ok and return unless user = User.find_by(email: params[:email]) + + user.send_reset_password_instructions + + head :ok + end + + def reset_password + head :bad_request and return if params[:reset_password_token].blank? || params[:password].blank? || params[:password_confirmation].blank? + + user = User.reset_password_by_token(params.slice(:reset_password_token, :password, :password_confirmation)) + if user.errors.empty? + head :ok + return + end + + render json: { errors: user.errors }, status: :unprocessable_entity + end + + def emails + render json: { emails: [ current_user.email ] } + end + + def server_providers + render json: { + server_providers: current_user.server_providers.includes(:server_provider_permissions).map do |server_provider| + { + id: server_provider.id, + name: server_provider.name, + permission: current_user.server_provider_permission(server_provider.id).permission + } + end + } + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 00000000..de6be794 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js new file mode 100644 index 00000000..0286f291 --- /dev/null +++ b/app/javascript/packs/application.js @@ -0,0 +1,8 @@ +// This file is automatically compiled by Webpack, along with any other files +// present in this directory. You're encouraged to place your actual application logic in +// a relevant structure within app/javascript and only use these pack files to reference +// that code so it'll be compiled. + +import App from '../src/app.module.es6'; + +App.create(); diff --git a/app/javascript/src/adapters/.gitkeep b/app/javascript/src/adapters/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/adapters/application.js b/app/javascript/src/adapters/application.js new file mode 100644 index 00000000..50b375f0 --- /dev/null +++ b/app/javascript/src/adapters/application.js @@ -0,0 +1,4 @@ +// Override the default adapter with the `DS.ActiveModelAdapter` which +// is built to work nicely with the ActiveModel::Serializers gem. +App.ApplicationAdapter = DS.ActiveModelAdapter.extend({ +}); diff --git a/app/javascript/src/app.js b/app/javascript/src/app.js new file mode 100644 index 00000000..b5af479f --- /dev/null +++ b/app/javascript/src/app.js @@ -0,0 +1,11 @@ +//= require_tree ./adapters +//= require_tree ./mixins +//= require_tree ./models +//= require_tree ./controllers +//= require_tree ./views +//= require_tree ./helpers +//= require_tree ./components +//= require_tree ./templates +//= require_tree ./routes +//= require ./router +//= require_self diff --git a/app/javascript/src/application.js b/app/javascript/src/application.js new file mode 100644 index 00000000..960c9c85 --- /dev/null +++ b/app/javascript/src/application.js @@ -0,0 +1,12 @@ +//= require jquery +//= require jquery_ujs +//= require ./environment +//= require ember +//= require ember-data +//= require active-model-adapter + +//= require_self +//= require ./app + +// for more details see: http://emberjs.com/guides/application/ +App = Ember.Application.create(); diff --git a/app/javascript/src/components/.gitkeep b/app/javascript/src/components/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/controllers/.gitkeep b/app/javascript/src/controllers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/environment.js b/app/javascript/src/environment.js new file mode 100644 index 00000000..3b0b6ca2 --- /dev/null +++ b/app/javascript/src/environment.js @@ -0,0 +1,7 @@ +window.EmberENV = { + // FEATURES: {}, + // EXTEND_PROTOTYPES: { + // Function: false, + // Array: true + // } +}; diff --git a/app/javascript/src/helpers/.gitkeep b/app/javascript/src/helpers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/initializers/.gitkeep b/app/javascript/src/initializers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/instance-initializers/.gitkeep b/app/javascript/src/instance-initializers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/mixins/.gitkeep b/app/javascript/src/mixins/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/models/.gitkeep b/app/javascript/src/models/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/router.js b/app/javascript/src/router.js new file mode 100644 index 00000000..0f89a424 --- /dev/null +++ b/app/javascript/src/router.js @@ -0,0 +1,5 @@ +// For more information see: http://emberjs.com/guides/routing/ + +App.Router.map(function() { + // this.resource('posts'); +}); diff --git a/app/javascript/src/routes/.gitkeep b/app/javascript/src/routes/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/serializers/.gitkeep b/app/javascript/src/serializers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/services/.gitkeep b/app/javascript/src/services/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/templates/.gitkeep b/app/javascript/src/templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/templates/components/.gitkeep b/app/javascript/src/templates/components/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/transforms/.gitkeep b/app/javascript/src/transforms/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/views/.gitkeep b/app/javascript/src/views/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 00000000..d394c3d1 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/jobs/sync_job.rb b/app/jobs/sync_job.rb new file mode 100644 index 00000000..9529fd36 --- /dev/null +++ b/app/jobs/sync_job.rb @@ -0,0 +1,20 @@ +require 'travis/vcs_proxy/syncer' + +class SyncJob < ApplicationJob + class SyncType + SERVER_PROVIDER = 1 + REPOSITORY = 2 + USER = 3 + end + + queue_as :default + + def perform(sync_type, id) + syncer = Travis::VcsProxy::Syncer.new + case sync_type + when SyncType::SERVER_PROVIDER then syncer.sync_server_provider(ServerProvider.find(id)) + when SyncType::REPOSITORY then syncer.sync_repository(Repository.find(id)) + when SyncType::USER then syncer.sync_user(User.find(id)) + end + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 00000000..286b2239 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 00000000..10a4cba8 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/models/p4_server_provider.rb b/app/models/p4_server_provider.rb new file mode 100644 index 00000000..732fb3bd --- /dev/null +++ b/app/models/p4_server_provider.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class P4ServerProvider < ServerProvider + has_settings :p4host, class_name: 'P4ServerSetting' +end diff --git a/app/models/p4_server_setting.rb b/app/models/p4_server_setting.rb new file mode 100644 index 00000000..d4c3aeb4 --- /dev/null +++ b/app/models/p4_server_setting.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class P4ServerSetting < RailsSettings::SettingsObject + validate do + errors.add(:url, 'is missing') if p4host.blank? + end +end diff --git a/app/models/ref.rb b/app/models/ref.rb new file mode 100644 index 00000000..7786b177 --- /dev/null +++ b/app/models/ref.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Ref < ApplicationRecord + BRANCH = 1 + TAG = 2 + + self.inheritance_column = nil + + belongs_to :repository + + validates_presence_of :name, :type, :repository_id + + scope :branch, -> { where(type: BRANCH) } + scope :tag, -> { where(type: TAG) } +end diff --git a/app/models/repository.rb b/app/models/repository.rb new file mode 100644 index 00000000..fd64e6f2 --- /dev/null +++ b/app/models/repository.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Repository < ApplicationRecord + belongs_to :server_provider + + validates_presence_of :name, :url, :server_provider_id + + has_many :refs, dependent: :destroy + has_many :permissions, class_name: 'RepositoryPermission', dependent: :delete_all + + def branches + refs.branch + end + + def tags + refs.tag + end +end diff --git a/app/models/repository_permission.rb b/app/models/repository_permission.rb new file mode 100644 index 00000000..b94bccfa --- /dev/null +++ b/app/models/repository_permission.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class RepositoryPermission < ApplicationRecord + belongs_to :repository + belongs_to :user + + READ = 1 + WRITE = 2 + ADMIN = 3 + SUPER = 4 +end diff --git a/app/models/server_provider.rb b/app/models/server_provider.rb new file mode 100644 index 00000000..ec604866 --- /dev/null +++ b/app/models/server_provider.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class ServerProvider < ApplicationRecord + has_many :server_provider_permissions, dependent: :destroy + has_many :users, through: :server_provider_permissions + + has_many :repositories, dependent: :destroy + + validates_presence_of :name, :url, :type +end diff --git a/app/models/server_provider_permission.rb b/app/models/server_provider_permission.rb new file mode 100644 index 00000000..37498c6f --- /dev/null +++ b/app/models/server_provider_permission.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ServerProviderPermission < ApplicationRecord + belongs_to :server_provider + belongs_to :user + + has_one :setting, class_name: 'ServerProviderUserSetting', foreign_key: :server_provider_user_id, dependent: :delete + + OWNER = 1 + MEMBER = 2 + + def owner? + permission == OWNER + end +end diff --git a/app/models/server_provider_user_setting.rb b/app/models/server_provider_user_setting.rb new file mode 100644 index 00000000..4da717ce --- /dev/null +++ b/app/models/server_provider_user_setting.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true +require 'P4' + +class ServerProviderUserSetting < ApplicationRecord + belongs_to :permission, class_name: 'ServerProviderPermission', foreign_key: :server_provider_user_id + validates_presence_of :username, :value, :server_provider_user_id + + validate :validate_connection + + def token=(token) + self.value = encryptor.encrypt_and_sign(token) + end + + def token + encryptor.decrypt_and_verify(value) + end + + private + + def encryptor + @encryptor ||= ActiveSupport::MessageEncryptor.new(Settings.p4_token_encryption_key) + end + + def validate_connection + file = Tempfile.new("p4ticket_#{id}") + file.write(token) + file.close + + ENV['P4TICKETS'] = file.path + + p4 = P4.new + p4.charset = 'utf8' + p4.port = permission.server_provider.url + p4.user = username + p4.connect + p4.run_login + rescue P4Exception => e + puts e.message.inspect + errors.add(:base, 'Connection failed') + ensure + if file + begin + file.close + file.unlink + rescue + end + end + + ENV.delete('P4TICKETS') + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 00000000..2ea40a53 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,45 @@ +class User < ApplicationRecord + include Devise::JWT::RevocationStrategies::JTIMatcher + + default_scope { where(active: true) } + + devise :two_factor_authenticatable, + :registerable, + :validatable, + :confirmable, + :jwt_authenticatable, + :two_factor_backupable, + :recoverable, + jwt_revocation_strategy: self, + otp_secret_encryption_key: Settings.otp_secret_encryption_key + + validate :password_complexity + + has_many :server_provider_permissions + has_many :server_providers, through: :server_provider_permissions + + has_many :repository_permissions + has_many :repositories, through: :repository_permissions + + def server_provider_permission(server_provider_id) + server_provider_permissions.find_by(server_provider_id: server_provider_id) + end + + def set_server_provider_permission(server_provider_id, permission) + perm = server_provider_permissions.first_or_initialize(server_provider_id: server_provider_id) + perm.permission = permission + perm.save + end + + def mark_as_deleted + update_columns(email: '', name: nil, active: false) + end + + private + + def password_complexity + return if password.blank? + + errors.add(:password, 'should contain a non-alphabet character (number or special character)') if password =~ /\A[A-Za-z]+\z/ + end +end diff --git a/app/serializers/application_serializer.rb b/app/serializers/application_serializer.rb new file mode 100644 index 00000000..7399b231 --- /dev/null +++ b/app/serializers/application_serializer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ApplicationSerializer + include JSONAPI::Serializer + + def to_h + data = serializable_hash + + if data[:data].is_a?(Hash) + data[:data][:attributes] + elsif data[:data].is_a?(Array) + data[:data].map{ |x| x[:attributes] } + elsif data[:data].nil? + nil + else + data + end + end +end diff --git a/app/serializers/full_ref_serializer.rb b/app/serializers/full_ref_serializer.rb new file mode 100644 index 00000000..af78b550 --- /dev/null +++ b/app/serializers/full_ref_serializer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class FullRefSerializer < ApplicationSerializer + attributes :id, :name + + attribute :type, -> (ref) { ref.type == Ref::BRANCH ? 'branch' : 'tag' } +end diff --git a/app/serializers/ref_serializer.rb b/app/serializers/ref_serializer.rb new file mode 100644 index 00000000..32091828 --- /dev/null +++ b/app/serializers/ref_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class RefSerializer < ApplicationSerializer + attributes :id, :name +end diff --git a/app/serializers/repository_serializer.rb b/app/serializers/repository_serializer.rb new file mode 100644 index 00000000..ae6800eb --- /dev/null +++ b/app/serializers/repository_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class RepositorySerializer < ApplicationSerializer + attributes :id, :name +end diff --git a/app/serializers/server_provider_serializer.rb b/app/serializers/server_provider_serializer.rb new file mode 100644 index 00000000..f39307ad --- /dev/null +++ b/app/serializers/server_provider_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ServerProviderSerializer < ApplicationSerializer + attributes :id, :name, :url, :type +end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb new file mode 100644 index 00000000..49d6c0fb --- /dev/null +++ b/app/serializers/user_serializer.rb @@ -0,0 +1,6 @@ +class UserSerializer < ApplicationSerializer + attributes :id, :name + + attribute(:login) { |user| user.email } + attribute(:emails) { |user| [ user.email ] } +end diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 00000000..dc55f64f --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 00000000..f6ca622e --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

Hello <%= @resource.email %>!

+ +

Someone has requested a link to change your password. You can do this through the link below.

+ +

<%= link_to 'Change my password', "http://localhost/edit_password?reset_password_token=#{@token}" %>

+ +

If you didn't request this, please ignore this email.

+

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb new file mode 100644 index 00000000..ba7ab887 --- /dev/null +++ b/app/views/devise/shared/_error_messages.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> +
+

+ <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase) + %> +

+ +
+<% end %> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 00000000..96a94124 --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_session_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %>
+ <% end %> +<% end %> diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 00000000..a71368e3 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,114 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../../Gemfile", __FILE__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_version + @bundler_version ||= + env_var_version || cli_arg_version || + lockfile_version + end + + def bundler_requirement + return "#{Gem::Requirement.default}.a" unless bundler_version + + bundler_gem_version = Gem::Version.new(bundler_version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") + + requirement += ".a" if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/rails b/bin/rails new file mode 100755 index 00000000..21d3e02d --- /dev/null +++ b/bin/rails @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +load File.expand_path("spring", __dir__) +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 00000000..7327f471 --- /dev/null +++ b/bin/rake @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +load File.expand_path("spring", __dir__) +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 00000000..90700ac4 --- /dev/null +++ b/bin/setup @@ -0,0 +1,36 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies + system! 'bin/yarn' + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:prepare' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/spring b/bin/spring new file mode 100755 index 00000000..b4147e84 --- /dev/null +++ b/bin/spring @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby +if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"]) + gem "bundler" + require "bundler" + + # Load Spring without loading other gems in the Gemfile, for speed. + Bundler.locked_gems&.specs&.find { |spec| spec.name == "spring" }&.tap do |spring| + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem "spring", spring.version + require "spring/binstub" + rescue Gem::LoadError + # Ignore when Spring is not installed. + end +end diff --git a/bin/webpack b/bin/webpack new file mode 100755 index 00000000..1031168d --- /dev/null +++ b/bin/webpack @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["NODE_ENV"] ||= "development" + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "bundler/setup" + +require "webpacker" +require "webpacker/webpack_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Webpacker::WebpackRunner.run(ARGV) +end diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server new file mode 100755 index 00000000..dd966273 --- /dev/null +++ b/bin/webpack-dev-server @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["NODE_ENV"] ||= "development" + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "bundler/setup" + +require "webpacker" +require "webpacker/dev_server_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Webpacker::DevServerRunner.run(ARGV) +end diff --git a/bin/yarn b/bin/yarn new file mode 100755 index 00000000..9fab2c35 --- /dev/null +++ b/bin/yarn @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + yarn = ENV["PATH"].split(File::PATH_SEPARATOR). + select { |dir| File.expand_path(dir) != __dir__ }. + product(["yarn", "yarn.cmd", "yarn.ps1"]). + map { |dir, file| File.expand_path(file, dir) }. + find { |file| File.executable?(file) } + + if yarn + exec yarn, *ARGV + else + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/config.ru b/config.ru new file mode 100644 index 00000000..4a3c09a6 --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 00000000..f2ace2e0 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,29 @@ +require_relative "boot" + +require "active_record/railtie" +require "action_controller/railtie" +require "action_mailer/railtie" +require "active_job/railtie" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module App + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 6.1 + + config.api_only = true + + config.active_job.queue_adapter = :sidekiq + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + config.eager_load_paths << Rails.root.join("lib") + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 00000000..3cda23b4 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 00000000..f39dc046 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: app_production diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 00000000..4357d00c --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +aC4UDQbVKxkKLXuKcWPJzNip+EmQKFA8dURE1krFhgrYcLs48gpnBk1ONgzMbZ6KDtlpktk/dCZTiShHi0ZgBapU5MEAP3GA1q/MAWCHbTZyzy5ph5JQYq86SBLaHpFU5mK31XHB5HyHduuPLhX6xmWDl0b16qv1lEXLy2zWgnFo55Wci90VkA8Fgzy5qJs9/nhtpAkFLeqpfA0o1hf4OEJo1KNrLQha0rda2d1bjVim4d6a726lRXUUhKRHpMAOlSmT8S+1XqR/RC3BGFEeS5iY8J2NIz33IY+RZblpcvhD+KC0lMFW3WiLOCYFL17CF/t+eLmVZ4FqVjXVsWVDnTy4UpAmHIHIwJ3BGVkg4TVH6UnpkyOoebYU0NfNcBmemUf73DZyTl1ZM9EQ334vYy0k+ZdrzvQxG2U1--uVh7a0W+6N8ICXG2--5pRUxbjUtTvQeJuxjCmS1Q== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 00000000..03c07209 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,19 @@ +default: &default + adapter: postgresql + encoding: unicode + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + host: <%= ENV['DB_HOST'] || 'localhost' %> + user: root + password: root + +development: + <<: *default + database: travis_vcs_proxy_development + +test: + <<: *default + database: travis_vcs_proxy_test + +production: + <<: *default + database: travis_vcs_proxy_production diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 00000000..cac53157 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 00000000..fb03653a --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,75 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp', 'caching-dev.txt').exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + # + config.debug_exception_response_format = :api + + config.action_mailer.default_url_options = { host: 'localhost' } + + config.action_mailer.delivery_method = :smtp + config.action_mailer.smtp_settings = { + address: 'mailhog', + port: 1025, + } +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 00000000..0231a6d6 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,104 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). + config.log_level = :info + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "app_production" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Log disallowed deprecations. + config.active_support.disallowed_deprecation = :log + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require "syslog/logger" + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Inserts middleware to perform automatic connection switching. + # The `database_selector` hash is used to pass options to the DatabaseSelector + # middleware. The `delay` is used to determine how long to wait after a write + # to send a subsequent read to the primary. + # + # The `database_resolver` class is used by the middleware to determine which + # database is appropriate to use based on the time delay. + # + # The `database_resolver_context` class is used by the middleware to set + # timestamps for the last write to the primary. The resolver uses the context + # class timestamps to determine how long to wait before reading from the + # replica. + # + # By default Rails will store a last write timestamp in the session. The + # DatabaseSelector middleware is designed as such you can define your own + # strategy for connection switching and pass that into the middleware through + # these configuration options. + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + config.action_mailer.default_url_options = { host: 'https://vcs-proxy.travis-ci.com' } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 00000000..70791305 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,57 @@ +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + config.cache_classes = false + config.action_view.cache_template_loading = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true +end diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 00000000..89d2efab --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100644 index 00000000..33699c30 --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code +# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". +Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] diff --git a/config/initializers/config.rb b/config/initializers/config.rb new file mode 100644 index 00000000..8c4347a9 --- /dev/null +++ b/config/initializers/config.rb @@ -0,0 +1,58 @@ +Config.setup do |config| + # Name of the constant exposing loaded settings + config.const_name = 'Settings' + + # Ability to remove elements of the array set in earlier loaded settings file. For example value: '--'. + # + # config.knockout_prefix = nil + + # Overwrite an existing value when merging a `nil` value. + # When set to `false`, the existing value is retained after merge. + # + # config.merge_nil_values = true + + # Overwrite arrays found in previously loaded settings file. When set to `false`, arrays will be merged. + # + # config.overwrite_arrays = true + + # Load environment variables from the `ENV` object and override any settings defined in files. + # + # config.use_env = false + + # Define ENV variable prefix deciding which variables to load into config. + # + # Reading variables from ENV is case-sensitive. If you define lowercase value below, ensure your ENV variables are + # prefixed in the same way. + # + # When not set it defaults to `config.const_name`. + # + config.env_prefix = 'SETTINGS' + + # What string to use as level separator for settings loaded from ENV variables. Default value of '.' works well + # with Heroku, but you might want to change it for example for '__' to easy override settings from command line, where + # using dots in variable names might not be allowed (eg. Bash). + # + # config.env_separator = '.' + + # Ability to process variables names: + # * nil - no change + # * :downcase - convert to lower case + # + # config.env_converter = :downcase + + # Parse numeric values as integers instead of strings. + # + # config.env_parse_values = true + + # Validate presence and type of specific config values. Check https://github.com/dry-rb/dry-validation for details. + # + # config.schema do + # required(:name).filled + # required(:age).maybe(:int?) + # required(:email).filled(format?: EMAIL_REGEX) + # end + + # Evaluate ERB in YAML config files at load time. + # + # config.evaluate_erb_yaml = true +end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 00000000..35d0f26f --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,30 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # If you are using webpack-dev-server then specify webpack-dev-server host +# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# If you are using UJS then enable automatic nonce generation +# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } + +# Set the nonce only to specific directives +# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 00000000..5a6a32d3 --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 00000000..1db6bafc --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,10 @@ +Rails.application.config.middleware.insert_before 0, Rack::Cors do + allow do + origins 'http://localhost:13007' + + resource '*', + headers: :any, + methods: [:get, :post, :put, :patch, :delete, :options, :head] + end +end + diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb new file mode 100644 index 00000000..9b5cf6bb --- /dev/null +++ b/config/initializers/devise.rb @@ -0,0 +1,289 @@ +# frozen_string_literal: true + +# Assuming you have not yet modified this file, each configuration option below +# is set to its default value. Note that some are commented out while others +# are not: uncommented lines are intended to protect your configuration from +# breaking changes in upgrades (i.e., in the event that future versions of +# Devise change the default values for those options). +# +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + config.warden do |manager| + manager.default_strategies(scope: :user).unshift(:two_factor_authenticatable, :two_factor_backupable) + end + + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '5701b8aeebd54f124d95a5b2f2e5b98079f3730558a0327aff4780b6cc585b3416f4d0c65107a74c362a640daa63006d70b33fa829337abeb3d4a5545301f680' + + # ==> Controller configuration + # Configure the parent class to the devise controllers. + # config.parent_controller = 'DeviseController' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:email] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:email] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. + # For API-only applications to support authentication "out-of-the-box", you will likely want to + # enable this with :database unless you are using a custom strategy. + # The supported strategies are: + # :database = Support basic authentication with authentication key + password + config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 12. If + # using other algorithms, it sets how many times you want the password to be hashed. + # The number of stretches used for generating the hashed password are stored + # with the hashed password. This allows you to change the stretches without + # invalidating existing passwords. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 12 + + # Set up a pepper to generate the hashed password. + # config.pepper = '1c1e453962fd6d7a5235fdc21fd6a37eb43908a7c929199412bf6a2b4e25ee3f60e8e6a53e387f96a6d783a336136e714fd4bfc65031a8cac4c72c0ecb2baf27' + + # Send a notification to the original email when the user's email is changed. + # config.send_email_changed_notification = false + + # Send a notification email when the user's password is changed. + config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. + # You can also set it to nil, which will allow the user to access the website + # without confirming their account. + # Default is 0.days, meaning the user cannot access the website without + # confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + config.confirm_within = 1.hour + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 1.hour + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # Is disabled because of 2FA + config.sign_in_after_reset_password = false + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html, should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' + + # ==> Configuration for :registerable + + # When set to false, does not sign a user in automatically after their password is + # changed. Defaults to true, so a user is signed in automatically after changing a password. + # config.sign_in_after_change_password = true + # + config.jwt do |jwt| + jwt.secret = Settings.jwt.secret + end +end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 00000000..a0b27b96 --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,6 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :otp_attempt +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 00000000..ac033bf9 --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb new file mode 100644 index 00000000..dc189968 --- /dev/null +++ b/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb new file mode 100644 index 00000000..00f64d71 --- /dev/null +++ b/config/initializers/permissions_policy.rb @@ -0,0 +1,11 @@ +# Define an application-wide HTTP permissions policy. For further +# information see https://developers.google.com/web/updates/2018/06/feature-policy +# +# Rails.application.config.permissions_policy do |f| +# f.camera :none +# f.gyroscope :none +# f.microphone :none +# f.usb :none +# f.fullscreen :self +# f.payment :self, "https://secure.example.com" +# end diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb new file mode 100644 index 00000000..bbfc3961 --- /dev/null +++ b/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml new file mode 100644 index 00000000..260e1c4b --- /dev/null +++ b/config/locales/devise.en.yml @@ -0,0 +1,65 @@ +# Additional translations at https://github.com/heartcombo/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + email_changed: + subject: "Email Changed" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." + updated: "Your account has been updated successfully." + updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 00000000..cf9b342d --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 00000000..d9b3e836 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,43 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +# +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 00000000..50ddabfb --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,47 @@ +Rails.application.routes.draw do + root to: 'home#index' + + scope :v1, module: :v1 do + devise_for :users, + controllers: { + sessions: 'v1/users/sessions', + registrations: 'v1/users/registrations', + confirmations: 'v1/users/confirmations' + }, + path_names: { + sign_in: 'login', + sign_out: 'logout', + register: 'sign_up' + } + + resource :user, only: [:show] do + collection do + get :emails + patch :update_email + patch :update_password + post :request_password_reset + post :reset_password + + get :server_providers + end + end + + resources :server_providers, only: [:create, :show, :update] do + member do + post :authenticate + post :forget + post :sync + + get :repositories + end + end + + resources :repositories, only: [:show] do + resources :branches, only: [:index, :show] + + member do + get :refs + end + end + end +end diff --git a/config/settings.yml b/config/settings.yml new file mode 100644 index 00000000..aa33041a --- /dev/null +++ b/config/settings.yml @@ -0,0 +1,5 @@ +jwt: + secret: fill-me +otp_secret_encryption_key: 12391230-12e90-2if912d120s912is0921iw9012iw9012wi1290wi1290f139 +p4_token_encryption_key: HTmcYwLgONSdedIPBrUcsVQjJjQkFZBE + diff --git a/config/settings/development.yml b/config/settings/development.yml new file mode 100644 index 00000000..e69de29b diff --git a/config/settings/production.yml b/config/settings/production.yml new file mode 100644 index 00000000..e69de29b diff --git a/config/settings/test.yml b/config/settings/test.yml new file mode 100644 index 00000000..e69de29b diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 00000000..db5bf130 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,6 @@ +Spring.watch( + ".ruby-version", + ".rbenv-vars", + "tmp/restart.txt", + "tmp/caching-dev.txt" +) diff --git a/config/webpack/development.js b/config/webpack/development.js new file mode 100644 index 00000000..c5edff94 --- /dev/null +++ b/config/webpack/development.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpack/environment.js b/config/webpack/environment.js new file mode 100644 index 00000000..d16d9af7 --- /dev/null +++ b/config/webpack/environment.js @@ -0,0 +1,3 @@ +const { environment } = require('@rails/webpacker') + +module.exports = environment diff --git a/config/webpack/production.js b/config/webpack/production.js new file mode 100644 index 00000000..be0f53aa --- /dev/null +++ b/config/webpack/production.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'production' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpack/test.js b/config/webpack/test.js new file mode 100644 index 00000000..c5edff94 --- /dev/null +++ b/config/webpack/test.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpacker.yml b/config/webpacker.yml new file mode 100644 index 00000000..a6b14656 --- /dev/null +++ b/config/webpacker.yml @@ -0,0 +1,92 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/javascript + source_entry_path: packs + public_root_path: public + public_output_path: packs + cache_path: tmp/cache/webpacker + webpack_compile_output: true + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + additional_paths: [] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + # Extract and emit a css file + extract_css: false + + static_assets_extensions: + - .jpg + - .jpeg + - .png + - .gif + - .tiff + - .ico + - .svg + - .eot + - .otf + - .ttf + - .woff + - .woff2 + + extensions: + - .mjs + - .js + - .sass + - .scss + - .css + - .module.sass + - .module.scss + - .module.css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + compile: true + + # Reference: https://webpack.js.org/configuration/dev-server/ + dev_server: + https: false + host: localhost + port: 3035 + public: localhost:3035 + hmr: false + # Inline should be set to true if using HMR + inline: true + overlay: true + compress: true + disable_host_check: true + use_local_ip: false + quiet: false + pretty: false + headers: + 'Access-Control-Allow-Origin': '*' + watch_options: + ignored: '**/node_modules/**' + + +test: + <<: *default + compile: true + + # Compile test packs to a separate directory + public_output_path: packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Extract and emit a css file + extract_css: true + + # Cache manifest.json for performance + cache_manifest: true diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 00000000..9cfa7f7e --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,134 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2021_06_17_063016) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "commits", force: :cascade do |t| + t.string "sha", null: false + t.integer "user_id", null: false + t.integer "repository_id", null: false + t.integer "ref_id", null: false + t.datetime "created_at", null: false + end + + create_table "jwt_deny_lists", force: :cascade do |t| + t.string "jti", null: false + t.datetime "exp", null: false + t.index ["jti"], name: "index_jwt_deny_lists_on_jti" + end + + create_table "oauth_access_tokens", force: :cascade do |t| + t.integer "resource_owner_id", null: false + t.integer "application_id", null: false + t.string "token" + t.string "refresh_token" + t.integer "expires_in" + t.datetime "created_at" + t.datetime "revoked_at" + end + + create_table "oauth_applications", force: :cascade do |t| + t.string "name", null: false + t.string "uuid", null: false + t.string "secret", null: false + t.string "redirect_uri", null: false + t.integer "owner_id" + end + + create_table "pull_requests", force: :cascade do |t| + t.integer "base_id", null: false + t.integer "head_id", null: false + t.integer "repository_id", null: false + t.integer "user_id", null: false + t.datetime "created_at" + t.integer "number" + end + + create_table "refs", force: :cascade do |t| + t.string "name", null: false + t.integer "type", null: false + t.integer "repository_id", null: false + end + + create_table "repositories", force: :cascade do |t| + t.string "name", null: false + t.string "url", null: false + t.integer "server_provider_id", null: false + end + + create_table "repository_permissions", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "repository_id", null: false + t.integer "permission", null: false + end + + create_table "server_provider_permissions", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "server_provider_id", null: false + t.integer "permission", null: false + end + + create_table "server_provider_user_settings", force: :cascade do |t| + t.string "value", null: false + t.integer "server_provider_user_id", null: false + t.boolean "is_syncing" + end + + create_table "server_providers", force: :cascade do |t| + t.string "name", null: false + t.string "url", null: false + t.string "type", null: false + t.string "listener_token" + end + + create_table "settings", force: :cascade do |t| + t.string "var", null: false + t.text "value" + t.string "target_type", null: false + t.bigint "target_id", null: false + t.datetime "created_at", precision: 6 + t.datetime "updated_at", precision: 6 + t.index ["target_type", "target_id", "var"], name: "index_settings_on_target_type_and_target_id_and_var", unique: true + t.index ["target_type", "target_id"], name: "index_settings_on_target" + end + + create_table "users", force: :cascade do |t| + t.string "name" + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "confirmation_token" + t.datetime "confirmation_sent_at" + t.string "type", null: false + t.datetime "created_at", null: false + t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true + t.index ["email"], name: "index_users_on_email", unique: true + end + + add_foreign_key "commits", "refs" + add_foreign_key "commits", "repositories" + add_foreign_key "commits", "users" + add_foreign_key "oauth_applications", "users", column: "owner_id" + add_foreign_key "pull_requests", "commits", column: "base_id" + add_foreign_key "pull_requests", "commits", column: "head_id" + add_foreign_key "pull_requests", "repositories" + add_foreign_key "pull_requests", "users" + add_foreign_key "refs", "repositories" + add_foreign_key "repositories", "server_providers" + add_foreign_key "repository_permissions", "repositories" + add_foreign_key "repository_permissions", "users" + add_foreign_key "server_provider_permissions", "server_providers" + add_foreign_key "server_provider_permissions", "users" + add_foreign_key "server_provider_user_settings", "server_provider_permissions", column: "server_provider_user_id" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 00000000..f3a0480d --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,7 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Examples: +# +# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) +# Character.create(name: 'Luke', movie: movies.first) diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..28da0827 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +bundle + +exec "$@" diff --git a/lib/assets/.keep b/lib/assets/.keep new file mode 100644 index 00000000..e69de29b diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 00000000..e69de29b diff --git a/lib/travis/vcs_proxy/p4_connection.rb b/lib/travis/vcs_proxy/p4_connection.rb new file mode 100644 index 00000000..c9e0ea9c --- /dev/null +++ b/lib/travis/vcs_proxy/p4_connection.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true +require 'P4' + +module Travis + module VcsProxy + class P4Connection + class PermissionNotFound < StandardError; end + + def initialize(server_provider, user, token) + @server_provider = server_provider + @user = user + @server_provider_permission = user.server_provider_permission(server_provider.id) + raise PermissionNotFound if @server_provider_permission.blank? + @token = token + end + + def repositories + p4.run_depots.map do |depot| + { + name: depot['name'] + } + end + rescue P4Exception => e + puts e.message.inspect + + [] + end + + def branches(repository_name) + p4.run_streams("//#{repository_name}/...").map do |stream| + { + name: stream['Stream'] + } + end + rescue P4Exception => e + puts e.message.inspect + + [] + end + + private + + def p4 + return @p4 if defined?(@p4) + + file = Tempfile.new("p4ticket_#{@server_provider.id}_#{@user.id}") + file.write(@token) + file.close + + p4 = P4.new + p4.charset = 'utf8' + p4.port = @server_provider.url + p4.user = @server_provider_permission.setting.username + p4.connect + p4.run_login + + p4 + rescue P4Exception => e + puts e.message.inspect + raise + ensure + file.unlink if file + ENV.delete('P4TICKETS') + end + end + end +end diff --git a/lib/travis/vcs_proxy/sync/repository.rb b/lib/travis/vcs_proxy/sync/repository.rb new file mode 100644 index 00000000..6c354b6d --- /dev/null +++ b/lib/travis/vcs_proxy/sync/repository.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Travis + module VcsProxy + module Sync + class Repository + def initialize(repository) + @repository = repository + end + + def sync + end + end + end + end +end diff --git a/lib/travis/vcs_proxy/sync/server_provider.rb b/lib/travis/vcs_proxy/sync/server_provider.rb new file mode 100644 index 00000000..5384eba4 --- /dev/null +++ b/lib/travis/vcs_proxy/sync/server_provider.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Travis + module VcsProxy + module Sync + class ServerProvider + def initialize(server_provider) + @server_provider = server_provider + end + + def sync + ActiveRecord::Base.transaction do + @server_provider.users.each do |user| + connection = Travis::VcsProxy::P4Connection.new(@server_provider, user, user.server_provider_permission(@server_provider.id).setting.token) + repos = connection.repositories.map do |repository| + repo = @server_provider.repositories.first_or_initialize(name: repository[:name]) + unless repo.persisted? + repo.url = 'STUB' + repo.save! + end + + repo + end + + repos.each do |repo| + connection.branches(repo.name).each do |branch| + repo.refs.find_or_create_by!(type: Ref::BRANCH, name: branch[:name].sub(/\A\/\/#{Regexp.escape(repo.name)}\//, '')) + end + end + end + end + end + end + end + end +end diff --git a/lib/travis/vcs_proxy/sync/user.rb b/lib/travis/vcs_proxy/sync/user.rb new file mode 100644 index 00000000..5728407a --- /dev/null +++ b/lib/travis/vcs_proxy/sync/user.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Travis + module VcsProxy + module Sync + class User + def initialize(user) + @user = user + end + + def sync + end + end + end + end +end diff --git a/lib/travis/vcs_proxy/syncer.rb b/lib/travis/vcs_proxy/syncer.rb new file mode 100644 index 00000000..a047e899 --- /dev/null +++ b/lib/travis/vcs_proxy/syncer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Travis + module VcsProxy + class Syncer + def sync_server_provider(server_provider) + Sync::ServerProvider.new(server_provider).sync + end + + def sync_user(user) + Sync::User.new(user).sync + end + + def sync_repository(repository) + Sync::Repository.new(repository).sync + end + end + end +end diff --git a/log/.keep b/log/.keep new file mode 100644 index 00000000..e69de29b diff --git a/public/404.html b/public/404.html new file mode 100644 index 00000000..2be3af26 --- /dev/null +++ b/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/422.html b/public/422.html new file mode 100644 index 00000000..c08eac0d --- /dev/null +++ b/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/500.html b/public/500.html new file mode 100644 index 00000000..78a030af --- /dev/null +++ b/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png new file mode 100644 index 00000000..e69de29b diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 00000000..e69de29b diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..c19f78ab --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/spec/jobs/sync_job_spec.rb b/spec/jobs/sync_job_spec.rb new file mode 100644 index 00000000..0e35afe1 --- /dev/null +++ b/spec/jobs/sync_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe SyncJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/jwt_deny_list_spec.rb b/spec/models/jwt_deny_list_spec.rb new file mode 100644 index 00000000..b89aa79b --- /dev/null +++ b/spec/models/jwt_deny_list_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe JwtDenyList, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 00000000..47a31bb4 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe User, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 00000000..e69de29b diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 00000000..e69de29b diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 00000000..e69de29b From f2adfa33bf29688605f0615320aa4a8634e4b275 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Mon, 28 Jun 2021 16:25:37 +0300 Subject: [PATCH 02/65] Set up 2FA when confirming account --- app/controllers/v1/users/confirmations_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/v1/users/confirmations_controller.rb b/app/controllers/v1/users/confirmations_controller.rb index 4cbc754e..f5f5c035 100644 --- a/app/controllers/v1/users/confirmations_controller.rb +++ b/app/controllers/v1/users/confirmations_controller.rb @@ -4,7 +4,7 @@ class V1::Users::ConfirmationsController < Devise::ConfirmationsController clear_respond_to respond_to :json - def create + def show resource = nil ActiveRecord::Base.transaction do resource = resource_class.confirm_by_token(params[:confirmation_token]) From 85f3dd52a61f0f4476b13862de4b7ec5829141c4 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Thu, 15 Jul 2021 19:16:23 +0300 Subject: [PATCH 03/65] Send token when creating session and confirming email Also, enable 2FA separately via a separate endpoint --- app/controllers/application_controller.rb | 10 ++++++ .../v1/users/confirmations_controller.rb | 20 +++--------- .../v1/users/registrations_controller.rb | 1 + .../v1/users/sessions_controller.rb | 5 ++- .../v1/users/two_factor_auth_controller.rb | 31 +++++++++++++++++++ app/controllers/v1/users_controller.rb | 1 + config/initializers/devise.rb | 10 ++++++ config/routes.rb | 8 +++++ 8 files changed, 68 insertions(+), 18 deletions(-) create mode 100644 app/controllers/v1/users/two_factor_auth_controller.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e23bde9f..d0515ac6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,10 +1,20 @@ class ApplicationController < ActionController::API + before_action :require_2fa + private def require_authentication head :unauthorized and return unless user_signed_in? end + def require_2fa + head :unauthorized and return unless two_factor_auth_enabled? + end + + def two_factor_auth_enabled? + !user_signed_in? || current_user.otp_required_for_login? + end + def presented_entity(resource_name, resource) "#{resource_name.to_s.classify}Serializer".constantize.new(resource).to_h end diff --git a/app/controllers/v1/users/confirmations_controller.rb b/app/controllers/v1/users/confirmations_controller.rb index f5f5c035..77849080 100644 --- a/app/controllers/v1/users/confirmations_controller.rb +++ b/app/controllers/v1/users/confirmations_controller.rb @@ -3,23 +3,13 @@ class V1::Users::ConfirmationsController < Devise::ConfirmationsController clear_respond_to respond_to :json + skip_before_action :require_2fa def show - resource = nil - ActiveRecord::Base.transaction do - resource = resource_class.confirm_by_token(params[:confirmation_token]) + user = User.confirm_by_token(params[:confirmation_token]) + render json: { errors: user.errors }, status: :unprocessable_entity and return if user.errors.present? - if resource.errors.empty? - resource.otp_required_for_login = true - resource.otp_secret = resource_class.generate_otp_secret - raise ActiveRecord::Rollback unless resource.save - else - raise ActiveRecord::Rollback - end - end - - head :ok and return if resource.errors.empty? - - render json: { errors: resource.errors }, status: :unprocessable_entity + warden.set_user(user) + render json: { token: request.env[Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY], otp_enabled: false } end end diff --git a/app/controllers/v1/users/registrations_controller.rb b/app/controllers/v1/users/registrations_controller.rb index 1bb8a7f3..97d2d793 100644 --- a/app/controllers/v1/users/registrations_controller.rb +++ b/app/controllers/v1/users/registrations_controller.rb @@ -3,6 +3,7 @@ class V1::Users::RegistrationsController < Devise::RegistrationsController clear_respond_to respond_to :json + skip_before_action :require_2fa def create build_resource(sign_up_params) diff --git a/app/controllers/v1/users/sessions_controller.rb b/app/controllers/v1/users/sessions_controller.rb index b22d0a58..c916afc0 100644 --- a/app/controllers/v1/users/sessions_controller.rb +++ b/app/controllers/v1/users/sessions_controller.rb @@ -3,13 +3,12 @@ class V1::Users::SessionsController < Devise::SessionsController clear_respond_to respond_to :json + skip_before_action :require_2fa - # Overriding because we don't need to send any answer def create resource = warden.authenticate!(auth_options) - sign_in(resource_name, resource) - head :ok + render json: { token: request.env[Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY], otp_enabled: resource.otp_required_for_login? } end private diff --git a/app/controllers/v1/users/two_factor_auth_controller.rb b/app/controllers/v1/users/two_factor_auth_controller.rb new file mode 100644 index 00000000..d685ee56 --- /dev/null +++ b/app/controllers/v1/users/two_factor_auth_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class V1::Users::TwoFactorAuthController < ApplicationController + before_action :require_authentication + skip_before_action :require_2fa, only: [:url, :enable] + + def url + if current_user.otp_secret.blank? + current_user.otp_secret = User.generate_otp_secret + render json: { errors: current_user.errors } and return unless current_user.save + end + + render json: { url: current_user.otp_provisioning_uri(current_user.email, issuer: 'Travis CI VCS Proxy') } + end + + def enable + current_user.otp_required_for_login = true + render json: { errors: current_user.errors } and return unless current_user.save + + User.revoke_jwt(nil, current_user) + warden.set_user(current_user) + render json: { token: request.env[Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY], otp_enabled: true } + end + + def codes + codes = current_user.generate_otp_backup_codes! + render json: { errors: current_user.errors } unless current_user.save + + render json: { codes: codes } + end +end \ No newline at end of file diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 604d92b3..0b4799a8 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -2,6 +2,7 @@ class V1::UsersController < ApplicationController before_action :require_authentication, except: [:request_password_reset, :reset_password] + skip_before_action :require_2fa, only: [:show, :request_password_reset, :reset_password] def show render json: presented_entity(:user, current_user) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 9b5cf6bb..10913826 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -285,5 +285,15 @@ # config.jwt do |jwt| jwt.secret = Settings.jwt.secret + + jwt.dispatch_requests = [ + ['POST', %r{^/v1/users/login$}], + ['GET', %r{^/v1/users/confirmation}], + ['POST', %r{^/v1/user/two_factor_auth/enable$}], + ] + + jwt.revocation_requests = [ + ['DELETE', %r{^/v1/users/logout$}], + ] end end diff --git a/config/routes.rb b/config/routes.rb index 50ddabfb..108f4394 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,6 +24,14 @@ get :server_providers end + + resource :two_factor_auth, controller: 'users/two_factor_auth', only: [] do + collection do + get :url + get :codes + post :enable + end + end end resources :server_providers, only: [:create, :show, :update] do From 08547505fcb6488a56412adb10f6886c724c5b88 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 16 Jul 2021 10:28:19 +0300 Subject: [PATCH 04/65] Return an empty token if 2FA is enabled and user didn't provide it --- app/controllers/application_controller.rb | 4 ++++ app/controllers/v1/users/confirmations_controller.rb | 2 +- app/controllers/v1/users/sessions_controller.rb | 9 +++++++-- app/controllers/v1/users/two_factor_auth_controller.rb | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d0515ac6..5301ba82 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -18,4 +18,8 @@ def two_factor_auth_enabled? def presented_entity(resource_name, resource) "#{resource_name.to_s.classify}Serializer".constantize.new(resource).to_h end + + def current_user_jwt_token + request.env[Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY] + end end diff --git a/app/controllers/v1/users/confirmations_controller.rb b/app/controllers/v1/users/confirmations_controller.rb index 77849080..a6b63b10 100644 --- a/app/controllers/v1/users/confirmations_controller.rb +++ b/app/controllers/v1/users/confirmations_controller.rb @@ -10,6 +10,6 @@ def show render json: { errors: user.errors }, status: :unprocessable_entity and return if user.errors.present? warden.set_user(user) - render json: { token: request.env[Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY], otp_enabled: false } + render json: { token: current_user_jwt_token, otp_enabled: false } end end diff --git a/app/controllers/v1/users/sessions_controller.rb b/app/controllers/v1/users/sessions_controller.rb index c916afc0..694d7935 100644 --- a/app/controllers/v1/users/sessions_controller.rb +++ b/app/controllers/v1/users/sessions_controller.rb @@ -6,9 +6,14 @@ class V1::Users::SessionsController < Devise::SessionsController skip_before_action :require_2fa def create - resource = warden.authenticate!(auth_options) + head :bad_request and return if !params.has_key?(:user) || params[:user][:email].blank? || params[:user][:password].blank? + user = User.find_by(email: params[:user][:email]) + head :unauthorized and return if user.blank? + render json: { token: '', otp_enabled: true } and return if user.valid_password?(params[:user][:password]) && user.otp_required_for_login? - render json: { token: request.env[Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY], otp_enabled: resource.otp_required_for_login? } + user = warden.authenticate!(auth_options) + + render json: { token: current_user_jwt_token, otp_enabled: user.otp_required_for_login? } end private diff --git a/app/controllers/v1/users/two_factor_auth_controller.rb b/app/controllers/v1/users/two_factor_auth_controller.rb index d685ee56..e2248592 100644 --- a/app/controllers/v1/users/two_factor_auth_controller.rb +++ b/app/controllers/v1/users/two_factor_auth_controller.rb @@ -19,7 +19,7 @@ def enable User.revoke_jwt(nil, current_user) warden.set_user(current_user) - render json: { token: request.env[Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY], otp_enabled: true } + render json: { token: current_user_jwt_token, otp_enabled: true } end def codes From ed7ba8db8a25250af82cc2c2539134700b519016 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 16 Jul 2021 15:44:43 +0300 Subject: [PATCH 05/65] Validate current OTP when enabling 2FA --- app/controllers/v1/users/two_factor_auth_controller.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/v1/users/two_factor_auth_controller.rb b/app/controllers/v1/users/two_factor_auth_controller.rb index e2248592..67e5a60c 100644 --- a/app/controllers/v1/users/two_factor_auth_controller.rb +++ b/app/controllers/v1/users/two_factor_auth_controller.rb @@ -7,15 +7,17 @@ class V1::Users::TwoFactorAuthController < ApplicationController def url if current_user.otp_secret.blank? current_user.otp_secret = User.generate_otp_secret - render json: { errors: current_user.errors } and return unless current_user.save + render json: { errors: current_user.errors }, status: :unprocessable_entity and return unless current_user.save end render json: { url: current_user.otp_provisioning_uri(current_user.email, issuer: 'Travis CI VCS Proxy') } end def enable + render json: { errors: [ 'Wrong OTP code' ] }, status: :unprocessable_entity and return unless params[:otp_attempt] == current_user.current_otp + current_user.otp_required_for_login = true - render json: { errors: current_user.errors } and return unless current_user.save + render json: { errors: current_user.errors }, status: :unprocessable_entity and return unless current_user.save User.revoke_jwt(nil, current_user) warden.set_user(current_user) @@ -24,7 +26,7 @@ def enable def codes codes = current_user.generate_otp_backup_codes! - render json: { errors: current_user.errors } unless current_user.save + render json: { errors: current_user.errors }, status: :unprocessable_entity unless current_user.save render json: { codes: codes } end From 35cbc113e4739a098d0b115efa5b925c2e15faf7 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 10:11:08 +0300 Subject: [PATCH 06/65] Add endpoint for resending confirmation email --- app/controllers/v1/users/confirmations_controller.rb | 11 +++++++++++ config/routes.rb | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/app/controllers/v1/users/confirmations_controller.rb b/app/controllers/v1/users/confirmations_controller.rb index a6b63b10..8bf597e7 100644 --- a/app/controllers/v1/users/confirmations_controller.rb +++ b/app/controllers/v1/users/confirmations_controller.rb @@ -12,4 +12,15 @@ def show warden.set_user(user) render json: { token: current_user_jwt_token, otp_enabled: false } end + + def resend + head :bad_request and return if params[:email].blank? + + user = User.find_by!(email: params[:email]) + head :ok and return if user.confirmed? + + user.resend_confirmation_instructions + + head :ok + end end diff --git a/config/routes.rb b/config/routes.rb index 108f4394..abe43cb8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,6 +14,12 @@ register: 'sign_up' } + scope 'users/confirmation' do + devise_scope :user do + post :resend, to: 'users/confirmations#resend', as: :resend_confirmation + end + end + resource :user, only: [:show] do collection do get :emails From 429784bc3ef1ee1a156fa2b13f42dcad20ea7672 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 10:18:00 +0300 Subject: [PATCH 07/65] Check current password when updating password --- app/controllers/v1/users_controller.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 0b4799a8..bde29b55 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -21,7 +21,12 @@ def update_email end def update_password - head :bad_request and return if params[:password].blank? || params[:password_confirmation].blank? + head :bad_request and return if params[:current_password].blank? || params[:password].blank? || params[:password_confirmation].blank? + + unless current_user.valid_password?(params[:current_password]) + render json: { errors: [ 'Invalid current password' ] }, status: :unprocessable_entity + return + end unless params[:password] == params[:password_confirmation] render json: { errors: [ 'Password does not match confirmation' ] }, status: :unprocessable_entity From e116bbd163ecf245e34cdf8a38576309954e7a8b Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Fri, 23 Jul 2021 10:22:24 +0300 Subject: [PATCH 08/65] Fix sessions, add fields to serializers --- app/controllers/v1/server_providers_controller.rb | 6 +++++- app/controllers/v1/users/sessions_controller.rb | 2 +- app/serializers/server_provider_serializer.rb | 2 ++ app/serializers/user_serializer.rb | 4 +++- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index 9213c7fd..544275c2 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -14,6 +14,7 @@ def create success = true klass = PROVIDER_KLASS[params[:server_provider][:type]] errors = [] + provider = nil ActiveRecord::Base.transaction do unless provider = klass.find_by(name: params[:server_provider][:name]) provider = klass.new(server_provider_params) @@ -31,7 +32,10 @@ def create end end - head :ok and return if success + data = presented_entity(:server_provider, provider) + data[:type] = PROVIDER_KLASS.invert[data[:type].constantize] + + render json: data and return if success render json: { errors: provider.errors }, status: :unprocessable_entity end diff --git a/app/controllers/v1/users/sessions_controller.rb b/app/controllers/v1/users/sessions_controller.rb index 694d7935..6e1c162a 100644 --- a/app/controllers/v1/users/sessions_controller.rb +++ b/app/controllers/v1/users/sessions_controller.rb @@ -9,7 +9,7 @@ def create head :bad_request and return if !params.has_key?(:user) || params[:user][:email].blank? || params[:user][:password].blank? user = User.find_by(email: params[:user][:email]) head :unauthorized and return if user.blank? - render json: { token: '', otp_enabled: true } and return if user.valid_password?(params[:user][:password]) && user.otp_required_for_login? + render json: { token: '', otp_enabled: true } and return if user.valid_password?(params[:user][:password]) && user.otp_required_for_login? && params[:user][:otp_attempt].blank? user = warden.authenticate!(auth_options) diff --git a/app/serializers/server_provider_serializer.rb b/app/serializers/server_provider_serializer.rb index f39307ad..f89ca5ba 100644 --- a/app/serializers/server_provider_serializer.rb +++ b/app/serializers/server_provider_serializer.rb @@ -2,4 +2,6 @@ class ServerProviderSerializer < ApplicationSerializer attributes :id, :name, :url, :type + + attribute(:repositories) { |server| server.repositories.map(&:id) } end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 49d6c0fb..3494a620 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -1,6 +1,8 @@ class UserSerializer < ApplicationSerializer - attributes :id, :name + attributes :id, :otp_required_for_login + attribute(:name) { |user| user.name || user.email } attribute(:login) { |user| user.email } attribute(:emails) { |user| [ user.email ] } + attribute(:servers) { |user| user.server_providers.map(&:id) } end From 296d81a78f00342455e464251242314d7314a604 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 10:23:37 +0300 Subject: [PATCH 09/65] Do not return 404 if user is not found when resending confirmation --- app/controllers/v1/users/confirmations_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/users/confirmations_controller.rb b/app/controllers/v1/users/confirmations_controller.rb index 8bf597e7..5eaffb5b 100644 --- a/app/controllers/v1/users/confirmations_controller.rb +++ b/app/controllers/v1/users/confirmations_controller.rb @@ -16,8 +16,8 @@ def show def resend head :bad_request and return if params[:email].blank? - user = User.find_by!(email: params[:email]) - head :ok and return if user.confirmed? + user = User.find_by(email: params[:email]) + head :ok and return if user.blank? || user.confirmed? user.resend_confirmation_instructions From 4babd7b64c22c26579386fa0888563e8291c2231 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Fri, 23 Jul 2021 11:14:00 +0300 Subject: [PATCH 10/65] Fix first_or_initialize --- app/controllers/v1/server_providers_controller.rb | 2 +- app/models/user.rb | 2 +- lib/travis/vcs_proxy/sync/server_provider.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index 544275c2..77189a5f 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -69,7 +69,7 @@ def authenticate success = true ActiveRecord::Base.transaction do - permission = current_user.server_provider_permissions.first_or_initialize(server_provider_id: @server_provider.id) + permission = current_user.server_provider_permissions.find_or_initialize_by(server_provider_id: @server_provider.id) unless permission.persisted? permission.permission = ServerProviderPermission::MEMBER unless permission.save diff --git a/app/models/user.rb b/app/models/user.rb index 2ea40a53..8c2c85e2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,7 +26,7 @@ def server_provider_permission(server_provider_id) end def set_server_provider_permission(server_provider_id, permission) - perm = server_provider_permissions.first_or_initialize(server_provider_id: server_provider_id) + perm = server_provider_permissions.find_or_initialize_by(server_provider_id: server_provider_id) perm.permission = permission perm.save end diff --git a/lib/travis/vcs_proxy/sync/server_provider.rb b/lib/travis/vcs_proxy/sync/server_provider.rb index 5384eba4..7e5b59da 100644 --- a/lib/travis/vcs_proxy/sync/server_provider.rb +++ b/lib/travis/vcs_proxy/sync/server_provider.rb @@ -13,7 +13,7 @@ def sync @server_provider.users.each do |user| connection = Travis::VcsProxy::P4Connection.new(@server_provider, user, user.server_provider_permission(@server_provider.id).setting.token) repos = connection.repositories.map do |repository| - repo = @server_provider.repositories.first_or_initialize(name: repository[:name]) + repo = @server_provider.repositories.find_or_initialize_by(name: repository[:name]) unless repo.persisted? repo.url = 'STUB' repo.save! From 01c69c738b20f540591cc1a9e36ab6a4b0bd7d77 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 12:54:55 +0300 Subject: [PATCH 11/65] Allow server level token to be added on creation/update --- .../v1/server_providers_controller.rb | 57 ++++++++++++++----- app/models/p4_server_provider.rb | 4 +- app/models/p4_server_setting.rb | 7 --- app/models/server_provider.rb | 14 +++++ app/models/server_provider_user_setting.rb | 30 ---------- app/services/validate_p4_credentials.rb | 47 +++++++++++++++ 6 files changed, 107 insertions(+), 52 deletions(-) delete mode 100644 app/models/p4_server_setting.rb create mode 100644 app/services/validate_p4_credentials.rb diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index 77189a5f..33a2cea4 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -11,22 +11,21 @@ class V1::ServerProvidersController < ApplicationController def create head :bad_request and return if params[:server_provider].blank? || !PROVIDER_KLASS.has_key?(params[:server_provider][:type]) - success = true klass = PROVIDER_KLASS[params[:server_provider][:type]] errors = [] provider = nil ActiveRecord::Base.transaction do - unless provider = klass.find_by(name: params[:server_provider][:name]) + unless provider = klass.find_by(url: params[:server_provider][:url]) provider = klass.new(server_provider_params) unless provider.save - success = false errors = provider.errors raise ActiveRecord::Rollback end end + set_provider_credentials(provider, errors) + unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission::OWNER) - success = false errors << 'Cannot set permission for user' raise ActiveRecord::Rollback end @@ -35,9 +34,9 @@ def create data = presented_entity(:server_provider, provider) data[:type] = PROVIDER_KLASS.invert[data[:type].constantize] - render json: data and return if success + render json: data and return if errors.blank? - render json: { errors: provider.errors }, status: :unprocessable_entity + render json: { errors: errors }, status: :unprocessable_entity end def show @@ -51,17 +50,19 @@ def update permission = current_user.server_provider_permission(@server_provider.id) head :forbidden and return if permission.blank? || !permission.owner? - update_params = server_provider_params.dup - if params.has_key?(:server_provider) && params[:server_provider][:token].present? - update_params[:listener_token] = params[:server_provider][:token] - end + errors = [] + ActiveRecord::Base.transaction do + unless @server_provider.update(server_provider_params) + errors = @server_provider.errors + raise ActiveRecord::Rollback + end - if @server_provider.update(update_params) - head :ok - return + set_provider_credentials(@server_provider, errors) end - render json: { errors: @server_provider.errors }, status: :unprocessable_entity + head :ok and return if errors.blank? + + render json: { errors: errors }, status: :unprocessable_entity end def authenticate @@ -78,6 +79,13 @@ def authenticate end end + begin + ValidateP4Credentials.new(params[:username], params[:token], @server_provider.url).call + rescue ValidateP4Credentials::ValidationFailed => e + success = false + raise ActiveRecord::Rollback + end + setting = permission.setting || permission.build_setting setting.token = params[:token] setting.username = params[:username] @@ -117,4 +125,25 @@ def server_provider_params def set_server_provider @server_provider = ServerProvider.find(params[:id]) end + + def set_provider_credentials(provider, errors) + return if params[:server_provider][:username].blank? || params[:server_provider][:token].blank? + + begin + ValidateP4Credentials.new(params[:server_provider][:username], params[:server_provider][:token], provider.url).call + rescue ValidateP4Credentials::ValidationFailed => e + success = false + errors << 'Cannot authenticate' + raise ActiveRecord::Rollback + end + + settings = provider.settings(:p4_host) + settings.username = params[:server_provider][:username] + settings.token = params[:server_provider][:token] + unless provider.save + success = false + errors << 'Cannot save credentials' + raise ActiveRecord::Rollback + end + end end diff --git a/app/models/p4_server_provider.rb b/app/models/p4_server_provider.rb index 732fb3bd..0590250a 100644 --- a/app/models/p4_server_provider.rb +++ b/app/models/p4_server_provider.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true class P4ServerProvider < ServerProvider - has_settings :p4host, class_name: 'P4ServerSetting' + has_settings(persistent: true) do |s| + s.key :p4_host + end end diff --git a/app/models/p4_server_setting.rb b/app/models/p4_server_setting.rb deleted file mode 100644 index d4c3aeb4..00000000 --- a/app/models/p4_server_setting.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class P4ServerSetting < RailsSettings::SettingsObject - validate do - errors.add(:url, 'is missing') if p4host.blank? - end -end diff --git a/app/models/server_provider.rb b/app/models/server_provider.rb index ec604866..7942494f 100644 --- a/app/models/server_provider.rb +++ b/app/models/server_provider.rb @@ -7,4 +7,18 @@ class ServerProvider < ApplicationRecord has_many :repositories, dependent: :destroy validates_presence_of :name, :url, :type + + has_settings(persistent: true) do |s| + s.key :generic + end + + before_save :generate_listener_token + + private + + def generate_listener_token + return if listener_token.present? + + self.listener_token = SecureRandom.hex(40) + end end diff --git a/app/models/server_provider_user_setting.rb b/app/models/server_provider_user_setting.rb index 4da717ce..8c035ed4 100644 --- a/app/models/server_provider_user_setting.rb +++ b/app/models/server_provider_user_setting.rb @@ -5,8 +5,6 @@ class ServerProviderUserSetting < ApplicationRecord belongs_to :permission, class_name: 'ServerProviderPermission', foreign_key: :server_provider_user_id validates_presence_of :username, :value, :server_provider_user_id - validate :validate_connection - def token=(token) self.value = encryptor.encrypt_and_sign(token) end @@ -20,32 +18,4 @@ def token def encryptor @encryptor ||= ActiveSupport::MessageEncryptor.new(Settings.p4_token_encryption_key) end - - def validate_connection - file = Tempfile.new("p4ticket_#{id}") - file.write(token) - file.close - - ENV['P4TICKETS'] = file.path - - p4 = P4.new - p4.charset = 'utf8' - p4.port = permission.server_provider.url - p4.user = username - p4.connect - p4.run_login - rescue P4Exception => e - puts e.message.inspect - errors.add(:base, 'Connection failed') - ensure - if file - begin - file.close - file.unlink - rescue - end - end - - ENV.delete('P4TICKETS') - end end diff --git a/app/services/validate_p4_credentials.rb b/app/services/validate_p4_credentials.rb new file mode 100644 index 00000000..26628d1b --- /dev/null +++ b/app/services/validate_p4_credentials.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +require 'P4' + +class ValidateP4Credentials + class ValidationFailed < StandardError + attr_reader :message + + def initialize(message) + @message = message + end + end + + def initialize(username, token, url) + @username = username + @token = token + @url = url + end + + def call + file = Tempfile.new('p4ticket') + file.write(@token) + file.close + + ENV['P4TICKETS'] = file.path + + p4 = P4.new + p4.charset = 'utf8' + p4.port = @url + p4.user = @username + p4.connect + p4.run_login + + nil + rescue P4Exception => e + raise ValidationFailed.new(e.message) + ensure + if file + begin + file.close + file.unlink + rescue + end + end + + ENV.delete('P4TICKETS') + end +end \ No newline at end of file From b781462516b42b157a74435dc24cce26ed5b55aa Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 14:24:43 +0300 Subject: [PATCH 12/65] Add endpoint for adding repository level token --- app/controllers/v1/repositories_controller.rb | 20 +++++++++++++++++++ app/models/concerns/p4_host_settings.rb | 11 ++++++++++ app/models/p4_server_provider.rb | 4 +--- app/models/repository_permission.rb | 7 +++---- app/models/user.rb | 4 ++++ config/routes.rb | 1 + 6 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 app/models/concerns/p4_host_settings.rb diff --git a/app/controllers/v1/repositories_controller.rb b/app/controllers/v1/repositories_controller.rb index 91a2b148..5d7886e6 100644 --- a/app/controllers/v1/repositories_controller.rb +++ b/app/controllers/v1/repositories_controller.rb @@ -12,6 +12,26 @@ def refs render json: @repository.refs.map { |ref| presented_entity(:full_ref, ref) } end + def token + head :bad_request and return if params[:username].blank? || params[:token].blank? + + permission = current_user.repository_permission(@repository.id) + head :forbidden and return if permission.blank? || (!permission.owner? && !permission.admin?) + + begin + ValidateP4Credentials.new(params[:username], params[:token], @repository.server_provider.url).call + rescue ValidateP4Credentials::ValidationFailed + render json: { errors: [ 'Cannot authenticate' ] }, status: :unprocessable_entity and return + end + + settings = permission.settings(:p4_host) + settings.username = params[:username] + settings.token = params[:token] + head :ok and return if permission.save + + render json: { errors: permission.errors }, status: :unprocessable_entity + end + private def set_repository diff --git a/app/models/concerns/p4_host_settings.rb b/app/models/concerns/p4_host_settings.rb new file mode 100644 index 00000000..447739e6 --- /dev/null +++ b/app/models/concerns/p4_host_settings.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module P4HostSettings + extend ActiveSupport::Concern + + included do + has_settings(persistent: true) do |s| + s.key :p4_host, defaults: { username: '', token: '' } + end + end +end \ No newline at end of file diff --git a/app/models/p4_server_provider.rb b/app/models/p4_server_provider.rb index 0590250a..1b77e40f 100644 --- a/app/models/p4_server_provider.rb +++ b/app/models/p4_server_provider.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true class P4ServerProvider < ServerProvider - has_settings(persistent: true) do |s| - s.key :p4_host - end + include P4HostSettings end diff --git a/app/models/repository_permission.rb b/app/models/repository_permission.rb index b94bccfa..fd46f9f9 100644 --- a/app/models/repository_permission.rb +++ b/app/models/repository_permission.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true class RepositoryPermission < ApplicationRecord + include P4HostSettings + belongs_to :repository belongs_to :user - READ = 1 - WRITE = 2 - ADMIN = 3 - SUPER = 4 + enum permission: [ :read, :write, :admin, :super ] end diff --git a/app/models/user.rb b/app/models/user.rb index 8c2c85e2..a32516ea 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -31,6 +31,10 @@ def set_server_provider_permission(server_provider_id, permission) perm.save end + def repository_permission(repository_id) + repository_permissions.find_by(repository_id: repository_id) + end + def mark_as_deleted update_columns(email: '', name: nil, active: false) end diff --git a/config/routes.rb b/config/routes.rb index abe43cb8..0cbd09a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,6 +55,7 @@ member do get :refs + patch :token end end end From d6397e3d3c4b94bbf8ed0a78b633a11e3df64d7b Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 14:35:42 +0300 Subject: [PATCH 13/65] Add last_synced_at to repository serializer --- app/serializers/repository_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/serializers/repository_serializer.rb b/app/serializers/repository_serializer.rb index ae6800eb..475b620e 100644 --- a/app/serializers/repository_serializer.rb +++ b/app/serializers/repository_serializer.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class RepositorySerializer < ApplicationSerializer - attributes :id, :name + attributes :id, :name, :last_synced_at end From af2eee1b1f65b6c035448043ba7691aef7c7f6e1 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 14:38:28 +0300 Subject: [PATCH 14/65] Update reset password email link --- app/views/devise/mailer/reset_password_instructions.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb index f6ca622e..21ee79f0 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.erb +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -2,7 +2,7 @@

Someone has requested a link to change your password. You can do this through the link below.

-

<%= link_to 'Change my password', "http://localhost/edit_password?reset_password_token=#{@token}" %>

+

<%= link_to 'Change my password', "http://localhost/setup_new_password?reset_password_token=#{@token}" %>

If you didn't request this, please ignore this email.

Your password won't change until you access the link above and create a new one.

From f5a1ec35742611036ff04c0b382ee39d86c54260 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 14:53:49 +0300 Subject: [PATCH 15/65] Encrypt token in server provider and repository permission settings --- app/controllers/v1/repositories_controller.rb | 5 ++--- .../v1/server_providers_controller.rb | 5 ++--- app/models/concerns/encrypted_token.rb | 19 +++++++++++++++++++ app/models/concerns/p4_host_settings.rb | 11 +++++++++++ app/models/p4_server_provider.rb | 1 + app/models/repository_permission.rb | 1 + app/models/server_provider_user_setting.rb | 14 +++++--------- 7 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 app/models/concerns/encrypted_token.rb diff --git a/app/controllers/v1/repositories_controller.rb b/app/controllers/v1/repositories_controller.rb index 5d7886e6..7f0a365c 100644 --- a/app/controllers/v1/repositories_controller.rb +++ b/app/controllers/v1/repositories_controller.rb @@ -24,9 +24,8 @@ def token render json: { errors: [ 'Cannot authenticate' ] }, status: :unprocessable_entity and return end - settings = permission.settings(:p4_host) - settings.username = params[:username] - settings.token = params[:token] + permission.settings(:p4_host).username = params[:username] + permission.token = params[:token] head :ok and return if permission.save render json: { errors: permission.errors }, status: :unprocessable_entity diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index 33a2cea4..6f663507 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -137,9 +137,8 @@ def set_provider_credentials(provider, errors) raise ActiveRecord::Rollback end - settings = provider.settings(:p4_host) - settings.username = params[:server_provider][:username] - settings.token = params[:server_provider][:token] + provider.settings(:p4_host).username = params[:server_provider][:username] + provider.token = params[:server_provider][:token] unless provider.save success = false errors << 'Cannot save credentials' diff --git a/app/models/concerns/encrypted_token.rb b/app/models/concerns/encrypted_token.rb new file mode 100644 index 00000000..6667bee2 --- /dev/null +++ b/app/models/concerns/encrypted_token.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module EncryptedToken + extend ActiveSupport::Concern + + def encrypted_token(token_value) + encryptor.encrypt_and_sign(token_value) + end + + def decrypted_token(token_value) + encryptor.decrypt_and_verify(token_value) + end + + private + + def encryptor + @encryptor ||= ActiveSupport::MessageEncryptor.new(Settings.p4_token_encryption_key) + end +end \ No newline at end of file diff --git a/app/models/concerns/p4_host_settings.rb b/app/models/concerns/p4_host_settings.rb index 447739e6..a8599aec 100644 --- a/app/models/concerns/p4_host_settings.rb +++ b/app/models/concerns/p4_host_settings.rb @@ -7,5 +7,16 @@ module P4HostSettings has_settings(persistent: true) do |s| s.key :p4_host, defaults: { username: '', token: '' } end + + def token=(token_value) + settings(:p4_host).token = encrypted_token(token_value) + end + + def token + tok = settings(:p4_host).token + return tok if tok.blank? + + decrypted_token(tok) + end end end \ No newline at end of file diff --git a/app/models/p4_server_provider.rb b/app/models/p4_server_provider.rb index 1b77e40f..04bf7751 100644 --- a/app/models/p4_server_provider.rb +++ b/app/models/p4_server_provider.rb @@ -2,4 +2,5 @@ class P4ServerProvider < ServerProvider include P4HostSettings + include EncryptedToken end diff --git a/app/models/repository_permission.rb b/app/models/repository_permission.rb index fd46f9f9..2be666ee 100644 --- a/app/models/repository_permission.rb +++ b/app/models/repository_permission.rb @@ -2,6 +2,7 @@ class RepositoryPermission < ApplicationRecord include P4HostSettings + include EncryptedToken belongs_to :repository belongs_to :user diff --git a/app/models/server_provider_user_setting.rb b/app/models/server_provider_user_setting.rb index 8c035ed4..93663f9b 100644 --- a/app/models/server_provider_user_setting.rb +++ b/app/models/server_provider_user_setting.rb @@ -2,20 +2,16 @@ require 'P4' class ServerProviderUserSetting < ApplicationRecord + include EncryptedToken + belongs_to :permission, class_name: 'ServerProviderPermission', foreign_key: :server_provider_user_id validates_presence_of :username, :value, :server_provider_user_id - def token=(token) - self.value = encryptor.encrypt_and_sign(token) + def token=(token_value) + self.value = encrypted_token(token_value) end def token - encryptor.decrypt_and_verify(value) - end - - private - - def encryptor - @encryptor ||= ActiveSupport::MessageEncryptor.new(Settings.p4_token_encryption_key) + decrypted_token(value) end end From 2ac7979d84e033fba1cc2b88a0802304210de31e Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 15:04:01 +0300 Subject: [PATCH 16/65] Add possibility to remove repository level token --- .../{ => repositories}/branches_controller.rb | 2 +- .../v1/repositories/token_controller.rb | 42 +++++++++++++++++++ app/controllers/v1/repositories_controller.rb | 21 +--------- app/models/concerns/p4_host_settings.rb | 2 +- config/routes.rb | 9 +++- 5 files changed, 52 insertions(+), 24 deletions(-) rename app/controllers/v1/{ => repositories}/branches_controller.rb (88%) create mode 100644 app/controllers/v1/repositories/token_controller.rb diff --git a/app/controllers/v1/branches_controller.rb b/app/controllers/v1/repositories/branches_controller.rb similarity index 88% rename from app/controllers/v1/branches_controller.rb rename to app/controllers/v1/repositories/branches_controller.rb index 8d57705c..cf1b2f61 100644 --- a/app/controllers/v1/branches_controller.rb +++ b/app/controllers/v1/repositories/branches_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class V1::BranchesController < ApplicationController +class V1::Repositories::BranchesController < ApplicationController before_action :require_authentication before_action :set_repository before_action :set_branch, only: [:show] diff --git a/app/controllers/v1/repositories/token_controller.rb b/app/controllers/v1/repositories/token_controller.rb new file mode 100644 index 00000000..ad73839b --- /dev/null +++ b/app/controllers/v1/repositories/token_controller.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class V1::Repositories::TokenController < ApplicationController + before_action :require_authentication + before_action :set_repository + + def set + head :bad_request and return if params[:username].blank? || params[:token].blank? + + permission = current_user.repository_permission(@repository.id) + head :forbidden and return if permission.blank? || (!permission.owner? && !permission.admin?) + + begin + ValidateP4Credentials.new(params[:username], params[:token], @repository.server_provider.url).call + rescue ValidateP4Credentials::ValidationFailed + render json: { errors: [ 'Cannot authenticate' ] }, status: :unprocessable_entity and return + end + + permission.settings(:p4_host).username = params[:username] + permission.token = params[:token] + head :ok and return if permission.save + + render json: { errors: permission.errors }, status: :unprocessable_entity + end + + def destroy + permission = current_user.repository_permission(@repository.id) + head :forbidden and return if permission.blank? || (!permission.owner? && !permission.admin?) + + permission.settings(:p4_host).username = nil + permission.token = nil + head :ok and return if permission.save + + render json: { errors: permission.errors }, status: :unprocessable_entity + end + + private + + def get_repository + @repository = current_user.repositories.find_by(id: params[:repository_id]) + end +end \ No newline at end of file diff --git a/app/controllers/v1/repositories_controller.rb b/app/controllers/v1/repositories_controller.rb index 7f0a365c..3281d831 100644 --- a/app/controllers/v1/repositories_controller.rb +++ b/app/controllers/v1/repositories_controller.rb @@ -2,7 +2,7 @@ class V1::RepositoriesController < ApplicationController before_action :require_authentication - before_action :set_repository, only: [:show, :refs] + before_action :set_repository def show render json: presented_entity(:repository, @repository) @@ -12,25 +12,6 @@ def refs render json: @repository.refs.map { |ref| presented_entity(:full_ref, ref) } end - def token - head :bad_request and return if params[:username].blank? || params[:token].blank? - - permission = current_user.repository_permission(@repository.id) - head :forbidden and return if permission.blank? || (!permission.owner? && !permission.admin?) - - begin - ValidateP4Credentials.new(params[:username], params[:token], @repository.server_provider.url).call - rescue ValidateP4Credentials::ValidationFailed - render json: { errors: [ 'Cannot authenticate' ] }, status: :unprocessable_entity and return - end - - permission.settings(:p4_host).username = params[:username] - permission.token = params[:token] - head :ok and return if permission.save - - render json: { errors: permission.errors }, status: :unprocessable_entity - end - private def set_repository diff --git a/app/models/concerns/p4_host_settings.rb b/app/models/concerns/p4_host_settings.rb index a8599aec..2d414b52 100644 --- a/app/models/concerns/p4_host_settings.rb +++ b/app/models/concerns/p4_host_settings.rb @@ -9,7 +9,7 @@ module P4HostSettings end def token=(token_value) - settings(:p4_host).token = encrypted_token(token_value) + settings(:p4_host).token = token_value.blank? ? token_value : encrypted_token(token_value) end def token diff --git a/config/routes.rb b/config/routes.rb index 0cbd09a2..67fc195c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -51,11 +51,16 @@ end resources :repositories, only: [:show] do - resources :branches, only: [:index, :show] + resources :branches, controller: 'repositories/branches', only: [:index, :show] + resources :token, controller: 'repositories/token', only: [] do + collection do + patch :set + delete :destroy + end + end member do get :refs - patch :token end end end From 13113724935efa4b732cc6fe1e1aa7c4ecd223c7 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 15:04:54 +0300 Subject: [PATCH 17/65] Use common Rails route for updating repo token --- app/controllers/v1/repositories/token_controller.rb | 2 +- config/routes.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/repositories/token_controller.rb b/app/controllers/v1/repositories/token_controller.rb index ad73839b..61f2f2b5 100644 --- a/app/controllers/v1/repositories/token_controller.rb +++ b/app/controllers/v1/repositories/token_controller.rb @@ -4,7 +4,7 @@ class V1::Repositories::TokenController < ApplicationController before_action :require_authentication before_action :set_repository - def set + def update head :bad_request and return if params[:username].blank? || params[:token].blank? permission = current_user.repository_permission(@repository.id) diff --git a/config/routes.rb b/config/routes.rb index 67fc195c..1f94c1a1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -54,7 +54,7 @@ resources :branches, controller: 'repositories/branches', only: [:index, :show] resources :token, controller: 'repositories/token', only: [] do collection do - patch :set + patch :update delete :destroy end end From d22d5a9c3d05d6d7b36f84e5a39e4e311dfa4994 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 15:29:00 +0300 Subject: [PATCH 18/65] Do not update existing server provider in creation form --- app/controllers/v1/server_providers_controller.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index 6f663507..8f1efe99 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -12,15 +12,15 @@ def create head :bad_request and return if params[:server_provider].blank? || !PROVIDER_KLASS.has_key?(params[:server_provider][:type]) klass = PROVIDER_KLASS[params[:server_provider][:type]] + render json: { errors: [ 'A server with this URL already exists.' ] }, status: :unprocessable_entity and return if klass.find_by(url: params[:server_provider][:url]).present? + errors = [] provider = nil ActiveRecord::Base.transaction do - unless provider = klass.find_by(url: params[:server_provider][:url]) - provider = klass.new(server_provider_params) - unless provider.save - errors = provider.errors - raise ActiveRecord::Rollback - end + provider = klass.new(server_provider_params) + unless provider.save + errors = provider.errors + raise ActiveRecord::Rollback end set_provider_credentials(provider, errors) From 51a678feff519e61e4e4763964ccfe88031f4684 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 15:30:23 +0300 Subject: [PATCH 19/65] Validate current password when removing account --- app/controllers/v1/users/registrations_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/v1/users/registrations_controller.rb b/app/controllers/v1/users/registrations_controller.rb index 97d2d793..f66ebdcb 100644 --- a/app/controllers/v1/users/registrations_controller.rb +++ b/app/controllers/v1/users/registrations_controller.rb @@ -23,6 +23,8 @@ def update end def destroy + render json: { errors: [ 'Invalid credentials' ] }, status: :unprocessable_entity and return unless resource.valid_password?(params[:password]) + resource.mark_as_deleted sign_out head :ok From 4e95fbf0a535cb5dd12ea5aa91e7cef2970ad22e Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 15:35:44 +0300 Subject: [PATCH 20/65] Add endpoint for searching server provider by URL --- app/controllers/v1/server_providers_controller.rb | 6 ++++++ config/routes.rb | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index 8f1efe99..f62884fa 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -116,6 +116,12 @@ def repositories render json: @server_provider.repositories.map { |repository| presented_entity(:repository, repository) } end + def by_url + head :bad_request and return if params[:url].blank? + + render json: presented_entity(:server_provider, ServerProvider.find_by!(url: params[:url])) + end + private def server_provider_params diff --git a/config/routes.rb b/config/routes.rb index 1f94c1a1..43ad823a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -41,6 +41,10 @@ end resources :server_providers, only: [:create, :show, :update] do + collection do + get :by_url + end + member do post :authenticate post :forget From 1a1d85704aebdf73c35b8c4955321f6d72609bb8 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 16:01:31 +0300 Subject: [PATCH 21/65] Redirect to confirmed page from email confirmation --- app/controllers/v1/users/confirmations_controller.rb | 9 ++++++--- .../devise/mailer/reset_password_instructions.html.erb | 2 +- config/application.rb | 2 ++ config/environments/development.rb | 2 -- config/environments/production.rb | 1 - config/initializers/{config.rb => 000_config.rb} | 0 config/initializers/cors.rb | 2 +- config/settings.yml | 3 ++- config/settings/development.yml | 2 ++ 9 files changed, 14 insertions(+), 9 deletions(-) rename config/initializers/{config.rb => 000_config.rb} (100%) diff --git a/app/controllers/v1/users/confirmations_controller.rb b/app/controllers/v1/users/confirmations_controller.rb index 5eaffb5b..5dcc62bf 100644 --- a/app/controllers/v1/users/confirmations_controller.rb +++ b/app/controllers/v1/users/confirmations_controller.rb @@ -7,10 +7,13 @@ class V1::Users::ConfirmationsController < Devise::ConfirmationsController def show user = User.confirm_by_token(params[:confirmation_token]) - render json: { errors: user.errors }, status: :unprocessable_entity and return if user.errors.present? + if user.errors.present? + redirect_uri = URI.join(Settings.web_url, 'unconfirmed') + redirect_uri.query = 'error=expired' + redirect_to redirect_uri.to_s and return + end - warden.set_user(user) - render json: { token: current_user_jwt_token, otp_enabled: false } + redirect_to URI.join(Settings.web_url, 'confirmed').to_s end def resend diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb index 21ee79f0..98986a10 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.erb +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -2,7 +2,7 @@

Someone has requested a link to change your password. You can do this through the link below.

-

<%= link_to 'Change my password', "http://localhost/setup_new_password?reset_password_token=#{@token}" %>

+

<%= link_to 'Change my password', URI.join(Settings.web_url, "setup_new_password?reset_password_token=#{@token}" %>

If you didn't request this, please ignore this email.

Your password won't change until you access the link above and create a new one.

diff --git a/config/application.rb b/config/application.rb index f2ace2e0..f1279167 100644 --- a/config/application.rb +++ b/config/application.rb @@ -25,5 +25,7 @@ class Application < Rails::Application # # config.time_zone = "Central Time (US & Canada)" config.eager_load_paths << Rails.root.join("lib") + + config.action_mailer.default_url_options = { host: Settings.api_url } end end diff --git a/config/environments/development.rb b/config/environments/development.rb index fb03653a..16255fcd 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -65,8 +65,6 @@ # config.debug_exception_response_format = :api - config.action_mailer.default_url_options = { host: 'localhost' } - config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: 'mailhog', diff --git a/config/environments/production.rb b/config/environments/production.rb index 0231a6d6..01ecc946 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -100,5 +100,4 @@ # config.active_record.database_selector = { delay: 2.seconds } # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session - config.action_mailer.default_url_options = { host: 'https://vcs-proxy.travis-ci.com' } end diff --git a/config/initializers/config.rb b/config/initializers/000_config.rb similarity index 100% rename from config/initializers/config.rb rename to config/initializers/000_config.rb diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index 1db6bafc..a4320769 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -1,6 +1,6 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do - origins 'http://localhost:13007' + origins Settings.web_url resource '*', headers: :any, diff --git a/config/settings.yml b/config/settings.yml index aa33041a..c626dc66 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -2,4 +2,5 @@ jwt: secret: fill-me otp_secret_encryption_key: 12391230-12e90-2if912d120s912is0921iw9012iw9012wi1290wi1290f139 p4_token_encryption_key: HTmcYwLgONSdedIPBrUcsVQjJjQkFZBE - +web_url: https://travis-vcs-proxy.travis-ci.org +api_url: https://travis-vcs-proxy-api.travis-ci.org \ No newline at end of file diff --git a/config/settings/development.yml b/config/settings/development.yml index e69de29b..b06c73c2 100644 --- a/config/settings/development.yml +++ b/config/settings/development.yml @@ -0,0 +1,2 @@ +web_url: http://localhost:13007 +api_url: http://localhost:13006 \ No newline at end of file From b737cc182a4301fd0fb6b7e22b2f6222b6a13e63 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 17:54:13 +0300 Subject: [PATCH 22/65] Add all kinds of sync --- .../v1/repositories/token_controller.rb | 16 +++---- app/controllers/v1/repositories_controller.rb | 6 +++ .../v1/server_providers_controller.rb | 2 +- app/controllers/v1/users_controller.rb | 6 +++ app/jobs/sync_job.rb | 7 +-- app/models/repository.rb | 5 ++ app/models/repository_permission.rb | 3 -- config/routes.rb | 2 + lib/travis/vcs_proxy/p4_connection.rb | 43 +++++++++++++---- lib/travis/vcs_proxy/sync/repository.rb | 47 ++++++++++++++++++- lib/travis/vcs_proxy/sync/server_provider.rb | 44 +++++++++++------ lib/travis/vcs_proxy/sync/user.rb | 3 ++ lib/travis/vcs_proxy/syncer.rb | 12 +++-- 13 files changed, 151 insertions(+), 45 deletions(-) diff --git a/app/controllers/v1/repositories/token_controller.rb b/app/controllers/v1/repositories/token_controller.rb index 61f2f2b5..f83b25b0 100644 --- a/app/controllers/v1/repositories/token_controller.rb +++ b/app/controllers/v1/repositories/token_controller.rb @@ -16,22 +16,22 @@ def update render json: { errors: [ 'Cannot authenticate' ] }, status: :unprocessable_entity and return end - permission.settings(:p4_host).username = params[:username] - permission.token = params[:token] - head :ok and return if permission.save + @repository.settings(:p4_host).username = params[:username] + @repository.token = params[:token] + head :ok and return if @repository.save - render json: { errors: permission.errors }, status: :unprocessable_entity + render json: { errors: @repository.errors }, status: :unprocessable_entity end def destroy permission = current_user.repository_permission(@repository.id) head :forbidden and return if permission.blank? || (!permission.owner? && !permission.admin?) - permission.settings(:p4_host).username = nil - permission.token = nil - head :ok and return if permission.save + @repository.settings(:p4_host).username = nil + @repository.token = nil + head :ok and return if @repository.save - render json: { errors: permission.errors }, status: :unprocessable_entity + render json: { errors: @repository.errors }, status: :unprocessable_entity end private diff --git a/app/controllers/v1/repositories_controller.rb b/app/controllers/v1/repositories_controller.rb index 3281d831..82b28585 100644 --- a/app/controllers/v1/repositories_controller.rb +++ b/app/controllers/v1/repositories_controller.rb @@ -12,6 +12,12 @@ def refs render json: @repository.refs.map { |ref| presented_entity(:full_ref, ref) } end + def sync + SyncJob.perform_later(SyncJob::SyncType::REPOSITORY, @repository.id, current_user.id) + + head :ok + end + private def set_repository diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index f62884fa..b0fbc3e5 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -107,7 +107,7 @@ def forget end def sync - SyncJob.perform_later(SyncJob::SyncType::SERVER_PROVIDER, @server_provider.id) + SyncJob.perform_later(SyncJob::SyncType::SERVER_PROVIDER, @server_provider.id, current_user.id) head :ok end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index bde29b55..fe950c49 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -79,4 +79,10 @@ def server_providers end } end + + def sync + SyncJob.perform_later(SyncJob::SyncType::USER, current_user.id) + + head :ok + end end diff --git a/app/jobs/sync_job.rb b/app/jobs/sync_job.rb index 9529fd36..f0c5a20c 100644 --- a/app/jobs/sync_job.rb +++ b/app/jobs/sync_job.rb @@ -9,12 +9,13 @@ class SyncType queue_as :default - def perform(sync_type, id) - syncer = Travis::VcsProxy::Syncer.new + def perform(sync_type, id, user_id = nil) + user = user_id.present? ? User.find(user_id) : User.find(id) + syncer = Travis::VcsProxy::Syncer.new(user) case sync_type when SyncType::SERVER_PROVIDER then syncer.sync_server_provider(ServerProvider.find(id)) when SyncType::REPOSITORY then syncer.sync_repository(Repository.find(id)) - when SyncType::USER then syncer.sync_user(User.find(id)) + when SyncType::USER then syncer.sync_user end end end diff --git a/app/models/repository.rb b/app/models/repository.rb index fd64e6f2..a1051668 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1,11 +1,16 @@ # frozen_string_literal: true class Repository < ApplicationRecord + include P4HostSettings + include EncryptedToken + belongs_to :server_provider validates_presence_of :name, :url, :server_provider_id has_many :refs, dependent: :destroy + has_many :repository_permissions + has_many :users, through: :repository_permissions has_many :permissions, class_name: 'RepositoryPermission', dependent: :delete_all def branches diff --git a/app/models/repository_permission.rb b/app/models/repository_permission.rb index 2be666ee..5de70bf7 100644 --- a/app/models/repository_permission.rb +++ b/app/models/repository_permission.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class RepositoryPermission < ApplicationRecord - include P4HostSettings - include EncryptedToken - belongs_to :repository belongs_to :user diff --git a/config/routes.rb b/config/routes.rb index 43ad823a..a3d755b0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,6 +27,7 @@ patch :update_password post :request_password_reset post :reset_password + post :sync get :server_providers end @@ -65,6 +66,7 @@ member do get :refs + post :sync end end end diff --git a/lib/travis/vcs_proxy/p4_connection.rb b/lib/travis/vcs_proxy/p4_connection.rb index c9e0ea9c..3065ade3 100644 --- a/lib/travis/vcs_proxy/p4_connection.rb +++ b/lib/travis/vcs_proxy/p4_connection.rb @@ -6,16 +6,14 @@ module VcsProxy class P4Connection class PermissionNotFound < StandardError; end - def initialize(server_provider, user, token) - @server_provider = server_provider - @user = user - @server_provider_permission = user.server_provider_permission(server_provider.id) - raise PermissionNotFound if @server_provider_permission.blank? + def initialize(url, username, token) + @url = url + @username = username @token = token end def repositories - p4.run_depots.map do |depot| + @repositories ||= p4.run_depots.map do |depot| { name: depot['name'] } @@ -27,7 +25,7 @@ def repositories end def branches(repository_name) - p4.run_streams("//#{repository_name}/...").map do |stream| + @branches ||= p4.run_streams("//#{repository_name}/...").map do |stream| { name: stream['Stream'] } @@ -38,19 +36,44 @@ def branches(repository_name) [] end + def users + @users ||= p4.run_users('-a').map do |user| + next unless user['Email'].include?('@') + + { + email: user['Email'], + name: user['User'] + } + end.compact + rescue P4Exception => e + puts e.message.inspect + + [] + end + + def permissions(repository_name) + @permissions ||= users.each_with_object({}) do |user, memo| + memo[user[:email]] = p4.run_protects('-u', user[:name], '-M', "//#{repository_name}/...").first['permMax'] + end + rescue P4Exception => e + puts e.message.inspect + + {} + end + private def p4 return @p4 if defined?(@p4) - file = Tempfile.new("p4ticket_#{@server_provider.id}_#{@user.id}") + file = Tempfile.new('p4ticket') file.write(@token) file.close p4 = P4.new p4.charset = 'utf8' - p4.port = @server_provider.url - p4.user = @server_provider_permission.setting.username + p4.port = @url + p4.user = @username p4.connect p4.run_login diff --git a/lib/travis/vcs_proxy/sync/repository.rb b/lib/travis/vcs_proxy/sync/repository.rb index 6c354b6d..0944beb9 100644 --- a/lib/travis/vcs_proxy/sync/repository.rb +++ b/lib/travis/vcs_proxy/sync/repository.rb @@ -4,11 +4,56 @@ module Travis module VcsProxy module Sync class Repository - def initialize(repository) + def initialize(repository, user) @repository = repository + @user = user end def sync + username = @repository.settings(:p4_host).username + token = @repository.token + if username.blank? || token.blank? + return unless server_provider_permission = @user.server_provider_permission(@repository.server_provider_id) + return unless server_provider_user_setting = server_provider_permission.setting + + username = server_provider_user_setting.username + token = server_provider_user_setting.token + end + return if username.blank? || token.blank? + + connection = Travis::VcsProxy::P4Connection.new(@repository.server_provider.url, username, token) + connection.branches(@repository.name).each do |branch| + @repository.refs.find_or_create_by!(type: Ref::BRANCH, name: branch[:name].sub(/\A\/\/#{Regexp.escape(@repository.name)}\//, '')) + end + + perms = connection.permissions(@repository.name) + Sidekiq.logger.debug "PERMS: #{perms.inspect}" + return if perms.blank? + repo_emails = perms.keys + users = ::User.where(email: repo_emails).group_by(&:email) + db_emails = @repository.users.pluck(:email) + + # Remove users that don't have access anymore + (db_emails - repo_emails).each do |email| + users[email].first.repository_permission(@repository.id).delete + end + + perms.each do |email, permission| + next unless users.has_key?(email) + user = users[email].first + perm = user.repository_permission(@repository.id) + if permission == 'none' + perm&.delete + return + end + + perm ||= user.repository_permissions.build(repository_id: @repository.id) + perm.permission = permission + perm.save! + end + + @repository.last_synced_at = Time.now + @repository.save! end end end diff --git a/lib/travis/vcs_proxy/sync/server_provider.rb b/lib/travis/vcs_proxy/sync/server_provider.rb index 7e5b59da..def7fe35 100644 --- a/lib/travis/vcs_proxy/sync/server_provider.rb +++ b/lib/travis/vcs_proxy/sync/server_provider.rb @@ -4,30 +4,44 @@ module Travis module VcsProxy module Sync class ServerProvider - def initialize(server_provider) + def initialize(server_provider, user) @server_provider = server_provider + @user = user end def sync ActiveRecord::Base.transaction do + server_provider_username = @server_provider.settings(:p4_host).username + server_provider_token = @server_provider.token + if server_provider_username.present? && server_provider_token.present? + connection = Travis::VcsProxy::P4Connection.new(@server_provider.url, server_provider_username, server_provider_token) + sync_connection_data(connection) + return + end + @server_provider.users.each do |user| - connection = Travis::VcsProxy::P4Connection.new(@server_provider, user, user.server_provider_permission(@server_provider.id).setting.token) - repos = connection.repositories.map do |repository| - repo = @server_provider.repositories.find_or_initialize_by(name: repository[:name]) - unless repo.persisted? - repo.url = 'STUB' - repo.save! - end + next unless permission_setting = user.server_provider_permission(@server_provider.id).setting + connection = Travis::VcsProxy::P4Connection.new(@server_provider.url, permission_setting.username, permission_setting.token) + sync_connection_data(connection) + end + end + end - repo - end + private - repos.each do |repo| - connection.branches(repo.name).each do |branch| - repo.refs.find_or_create_by!(type: Ref::BRANCH, name: branch[:name].sub(/\A\/\/#{Regexp.escape(repo.name)}\//, '')) - end - end + def sync_connection_data(connection) + repos = connection.repositories.map do |repository| + repo = @server_provider.repositories.find_or_initialize_by(name: repository[:name]) + unless repo.persisted? + repo.url = 'STUB' + repo.save! end + + repo + end + + repos.each do |repo| + Travis::VcsProxy::Sync::Repository.new(repo, @user).sync end end end diff --git a/lib/travis/vcs_proxy/sync/user.rb b/lib/travis/vcs_proxy/sync/user.rb index 5728407a..bbbe33b1 100644 --- a/lib/travis/vcs_proxy/sync/user.rb +++ b/lib/travis/vcs_proxy/sync/user.rb @@ -9,6 +9,9 @@ def initialize(user) end def sync + @user.server_providers.each do |server_provider| + Travis::VcsProxy::Sync::ServerProvider.new(server_provider, @user).sync + end end end end diff --git a/lib/travis/vcs_proxy/syncer.rb b/lib/travis/vcs_proxy/syncer.rb index a047e899..2ce5ecb1 100644 --- a/lib/travis/vcs_proxy/syncer.rb +++ b/lib/travis/vcs_proxy/syncer.rb @@ -3,16 +3,20 @@ module Travis module VcsProxy class Syncer + def initialize(user) + @user = user + end + def sync_server_provider(server_provider) - Sync::ServerProvider.new(server_provider).sync + Sync::ServerProvider.new(server_provider, @user).sync end - def sync_user(user) - Sync::User.new(user).sync + def sync_user + Sync::User.new(@user).sync end def sync_repository(repository) - Sync::Repository.new(repository).sync + Sync::Repository.new(repository, @user).sync end end end From e9fdeedf0124d751ecb396d74829fa1510ac6788 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 23 Jul 2021 18:26:55 +0300 Subject: [PATCH 23/65] Render file contents --- app/controllers/v1/repositories_controller.rb | 11 +++++++++++ app/controllers/v1/users_controller.rb | 4 ++++ config/routes.rb | 2 ++ lib/travis/vcs_proxy/p4_connection.rb | 7 +++++++ 4 files changed, 24 insertions(+) diff --git a/app/controllers/v1/repositories_controller.rb b/app/controllers/v1/repositories_controller.rb index 82b28585..9aecc293 100644 --- a/app/controllers/v1/repositories_controller.rb +++ b/app/controllers/v1/repositories_controller.rb @@ -12,6 +12,17 @@ def refs render json: @repository.refs.map { |ref| presented_entity(:full_ref, ref) } end + def content + head :bad_request and return if params[:ref].blank? || params[:path].blank? + + connection = Travis::VcsProxy::P4Connection.new(@repository.server_provider.url, @repository.server_provider.settings(:p4_host).username, @repository.server_provider.token) + + result = connection.file_contents(@repository.name, params[:path], params[:ref].presence) + render json: { errors: [ 'Cannot render file' ] }, status: :unprocessable_entity and return if result.blank? + + render plain: result[1] + end + def sync SyncJob.perform_later(SyncJob::SyncType::REPOSITORY, @repository.id, current_user.id) diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index fe950c49..a3935c97 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -80,6 +80,10 @@ def server_providers } end + def repositories + render json: current_user.repositories.map { |repository| presented_entity(:repository, repository) } + end + def sync SyncJob.perform_later(SyncJob::SyncType::USER, current_user.id) diff --git a/config/routes.rb b/config/routes.rb index a3d755b0..719b23d4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -30,6 +30,7 @@ post :sync get :server_providers + get :repositories end resource :two_factor_auth, controller: 'users/two_factor_auth', only: [] do @@ -66,6 +67,7 @@ member do get :refs + get 'content/:ref/(*path)', action: :content, format: false post :sync end end diff --git a/lib/travis/vcs_proxy/p4_connection.rb b/lib/travis/vcs_proxy/p4_connection.rb index 3065ade3..d126942e 100644 --- a/lib/travis/vcs_proxy/p4_connection.rb +++ b/lib/travis/vcs_proxy/p4_connection.rb @@ -61,6 +61,13 @@ def permissions(repository_name) {} end + def file_contents(repository_name, path, ref) + p4.run_print("//#{repository_name}/#{path}##{ref}") + rescue P4Exception => e + puts e.message.inspect + nil + end + private def p4 From f96fe5997f5529d9739b872edb49fea46d596f9c Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Mon, 26 Jul 2021 09:44:52 +0300 Subject: [PATCH 24/65] Fix get_file_contents API call --- app/controllers/v1/repositories_controller.rb | 2 +- lib/travis/vcs_proxy/p4_connection.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/v1/repositories_controller.rb b/app/controllers/v1/repositories_controller.rb index 9aecc293..e389edc6 100644 --- a/app/controllers/v1/repositories_controller.rb +++ b/app/controllers/v1/repositories_controller.rb @@ -17,7 +17,7 @@ def content connection = Travis::VcsProxy::P4Connection.new(@repository.server_provider.url, @repository.server_provider.settings(:p4_host).username, @repository.server_provider.token) - result = connection.file_contents(@repository.name, params[:path], params[:ref].presence) + result = connection.file_contents(@repository.name, params[:ref], params[:path]) render json: { errors: [ 'Cannot render file' ] }, status: :unprocessable_entity and return if result.blank? render plain: result[1] diff --git a/lib/travis/vcs_proxy/p4_connection.rb b/lib/travis/vcs_proxy/p4_connection.rb index d126942e..c8b311bb 100644 --- a/lib/travis/vcs_proxy/p4_connection.rb +++ b/lib/travis/vcs_proxy/p4_connection.rb @@ -61,8 +61,8 @@ def permissions(repository_name) {} end - def file_contents(repository_name, path, ref) - p4.run_print("//#{repository_name}/#{path}##{ref}") + def file_contents(repository_name, ref, path) + p4.run_print("//#{repository_name}/#{ref}/#{path}") rescue P4Exception => e puts e.message.inspect nil From 19f78c8e1d70c5da17b62d6c5ad0503df7c60894 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Tue, 27 Jul 2021 17:46:49 +0300 Subject: [PATCH 25/65] Fix typo in TokenController Add RepositoryPermission#owner? method --- app/controllers/v1/repositories/token_controller.rb | 4 ++-- app/models/repository_permission.rb | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/repositories/token_controller.rb b/app/controllers/v1/repositories/token_controller.rb index f83b25b0..e2b048b5 100644 --- a/app/controllers/v1/repositories/token_controller.rb +++ b/app/controllers/v1/repositories/token_controller.rb @@ -36,7 +36,7 @@ def destroy private - def get_repository + def set_repository @repository = current_user.repositories.find_by(id: params[:repository_id]) end -end \ No newline at end of file +end diff --git a/app/models/repository_permission.rb b/app/models/repository_permission.rb index 5de70bf7..514ae786 100644 --- a/app/models/repository_permission.rb +++ b/app/models/repository_permission.rb @@ -5,4 +5,8 @@ class RepositoryPermission < ApplicationRecord belongs_to :user enum permission: [ :read, :write, :admin, :super ] + + def owner? + admin? || super? + end end From a555daa74a8cffc21c59d1a45d27b9918d0d3045 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Tue, 27 Jul 2021 17:56:19 +0300 Subject: [PATCH 26/65] Add pagination and adjust serializers for web UI --- Gemfile | 1 + Gemfile.lock | 13 +++++++ app/controllers/application_controller.rb | 2 +- .../concerns/paginated_collection.rb | 34 +++++++++++++++++++ .../v1/server_providers_controller.rb | 21 +++++------- app/controllers/v1/users_controller.rb | 14 +++----- app/serializers/repository_serializer.rb | 6 +++- app/serializers/server_provider_serializer.rb | 15 ++++++-- 8 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 app/controllers/concerns/paginated_collection.rb diff --git a/Gemfile b/Gemfile index f5456542..57c93812 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,7 @@ gem 'config' gem 'jsonapi-serializer' gem 'p4ruby' gem 'sidekiq' +gem 'kaminari' gem 'bootsnap', require: false diff --git a/Gemfile.lock b/Gemfile.lock index f2869fe9..bf113d92 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -141,6 +141,18 @@ GEM jsonapi-serializer (2.2.0) activesupport (>= 4.2) jwt (2.2.3) + kaminari (1.2.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.1) + kaminari-activerecord (= 1.2.1) + kaminari-core (= 1.2.1) + kaminari-actionview (1.2.1) + actionview + kaminari-core (= 1.2.1) + kaminari-activerecord (1.2.1) + activerecord + kaminari-core (= 1.2.1) + kaminari-core (1.2.1) ledermann-rails-settings (2.5.0) activerecord (>= 4.2) listen (3.5.1) @@ -282,6 +294,7 @@ DEPENDENCIES devise-two-factor factory_bot jsonapi-serializer + kaminari ledermann-rails-settings listen p4ruby diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5301ba82..85f5c2c1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -16,7 +16,7 @@ def two_factor_auth_enabled? end def presented_entity(resource_name, resource) - "#{resource_name.to_s.classify}Serializer".constantize.new(resource).to_h + "#{resource_name.to_s.classify}Serializer".constantize.new(resource, { params: { current_user: current_user } }).to_h end def current_user_jwt_token diff --git a/app/controllers/concerns/paginated_collection.rb b/app/controllers/concerns/paginated_collection.rb new file mode 100644 index 00000000..2346768c --- /dev/null +++ b/app/controllers/concerns/paginated_collection.rb @@ -0,0 +1,34 @@ +module PaginatedCollection + extend ActiveSupport::Concern + + def paginated_collection(resource_name, serializer, collection) + data = {} + limit = collection.limit_value + pagination = { + count: collection.total_count, + is_first: collection.first_page?, + is_last: collection.last_page?, + offset: (collection.current_page - 1) * limit, + limit: limit, + first: { + limit: limit, + offset: 0 + }, + last: { + limit: limit, + offset: (collection.total_pages - 1) * limit + }, + prev: nil, + next: nil + } + pagination[:prev] = { limit: limit, offset: (collection.prev_page - 1) * limit } if collection.prev_page + pagination[:next] = { limit: limit, offset: collection.current_page * limit } if collection.next_page + + data[:meta] = { pagination: pagination } + data[resource_name] = collection.map do |item| + presented_entity(serializer, item) + end + + data + end +end \ No newline at end of file diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index b0fbc3e5..f844d110 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true class V1::ServerProvidersController < ApplicationController - PROVIDER_KLASS = { - 'perforce' => P4ServerProvider - }.freeze + include PaginatedCollection before_action :require_authentication before_action :set_server_provider, only: [:show, :update, :authenticate, :forget, :repositories, :sync] @@ -31,19 +29,13 @@ def create end end - data = presented_entity(:server_provider, provider) - data[:type] = PROVIDER_KLASS.invert[data[:type].constantize] - - render json: data and return if errors.blank? + render json: presented_entity(:server_provider, provider) and return if errors.blank? render json: { errors: errors }, status: :unprocessable_entity end def show - data = presented_entity(:server_provider, @server_provider) - data[:type] = PROVIDER_KLASS.invert[data[:type].constantize] - - render json: data + render json: presented_entity(:server_provider, @server_provider) end def update @@ -113,7 +105,12 @@ def sync end def repositories - render json: @server_provider.repositories.map { |repository| presented_entity(:repository, repository) } + order = params[:sort_by] == 'last_synced_at' ? 'DESC' : 'ASC' + repositories = @server_provider.repositories.order(params[:sort_by] => order) + unless params[:filter].empty? + repositories = repositories.where('name LIKE ?', "%#{params[:filter]}%") + end + render json: paginated_collection(:repositories, :repository, repositories.page(params[:page]).per(params[:limit])) end def by_url diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index a3935c97..1b47dc3b 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class V1::UsersController < ApplicationController + include PaginatedCollection + before_action :require_authentication, except: [:request_password_reset, :reset_password] skip_before_action :require_2fa, only: [:show, :request_password_reset, :reset_password] @@ -69,15 +71,9 @@ def emails end def server_providers - render json: { - server_providers: current_user.server_providers.includes(:server_provider_permissions).map do |server_provider| - { - id: server_provider.id, - name: server_provider.name, - permission: current_user.server_provider_permission(server_provider.id).permission - } - end - } + server_providers = current_user.server_providers.includes(:server_provider_permissions).page(params[:page]).per(params[:limit]) + + render json: paginated_collection(:server_providers, :server_provider, server_providers) end def repositories diff --git a/app/serializers/repository_serializer.rb b/app/serializers/repository_serializer.rb index 475b620e..79705ae3 100644 --- a/app/serializers/repository_serializer.rb +++ b/app/serializers/repository_serializer.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true class RepositorySerializer < ApplicationSerializer - attributes :id, :name, :last_synced_at + attributes :id, :name, :url, :token, :last_synced_at, :server_provider_id + + attributes(:permission) do |repo, params| + params[:current_user].repository_permission(repo.id).permission + end end diff --git a/app/serializers/server_provider_serializer.rb b/app/serializers/server_provider_serializer.rb index f89ca5ba..6427e26a 100644 --- a/app/serializers/server_provider_serializer.rb +++ b/app/serializers/server_provider_serializer.rb @@ -1,7 +1,18 @@ # frozen_string_literal: true class ServerProviderSerializer < ApplicationSerializer - attributes :id, :name, :url, :type + PROVIDER_KLASS = { + P4ServerProvider => 'perforce' + }.freeze - attribute(:repositories) { |server| server.repositories.map(&:id) } + PERMISSION = { + 1 => 'Owner', + 2 => 'User' + } + + attributes :id, :name, :url + + attribute(:type) { |server| PROVIDER_KLASS[server.type.constantize] } + attribute(:username) { |server| server.settings(:p4_host).username } + attribute(:permission) { |server, params| PERMISSION[params[:current_user].server_provider_permission(server.id).permission] } end From 652a5bf0c7e2399d6f83adfe8b17756869093f21 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Wed, 28 Jul 2021 13:41:20 +0300 Subject: [PATCH 27/65] Add server by URL + fixes and improvements --- app/controllers/v1/repositories_controller.rb | 2 +- .../v1/server_providers_controller.rb | 28 +++++++++++++++++-- app/controllers/v1/users_controller.rb | 7 ++++- app/serializers/repository_serializer.rb | 4 +-- app/serializers/server_provider_serializer.rb | 3 +- .../reset_password_instructions.html.erb | 2 +- config/routes.rb | 1 + 7 files changed, 39 insertions(+), 8 deletions(-) diff --git a/app/controllers/v1/repositories_controller.rb b/app/controllers/v1/repositories_controller.rb index e389edc6..285f15d0 100644 --- a/app/controllers/v1/repositories_controller.rb +++ b/app/controllers/v1/repositories_controller.rb @@ -32,6 +32,6 @@ def sync private def set_repository - @repository = current_user.repositories.find(params[:id]) + @repository = current_user.repositories.includes(:repository_permissions).find(params[:id]) end end diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index f844d110..c894dfb3 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -6,6 +6,10 @@ class V1::ServerProvidersController < ApplicationController before_action :require_authentication before_action :set_server_provider, only: [:show, :update, :authenticate, :forget, :repositories, :sync] + PROVIDER_KLASS = { + 'perforce' => P4ServerProvider + }.freeze + def create head :bad_request and return if params[:server_provider].blank? || !PROVIDER_KLASS.has_key?(params[:server_provider][:type]) @@ -106,10 +110,14 @@ def sync def repositories order = params[:sort_by] == 'last_synced_at' ? 'DESC' : 'ASC' - repositories = @server_provider.repositories.order(params[:sort_by] => order) + repositories = @server_provider.repositories + .includes(:repository_permissions) + .includes(:setting_objects) + .order(params[:sort_by] => order) unless params[:filter].empty? repositories = repositories.where('name LIKE ?', "%#{params[:filter]}%") end + render json: paginated_collection(:repositories, :repository, repositories.page(params[:page]).per(params[:limit])) end @@ -119,6 +127,22 @@ def by_url render json: presented_entity(:server_provider, ServerProvider.find_by!(url: params[:url])) end + def add_by_url + head :bad_request and return if params[:url].blank? + + errors = [] + provider = ServerProvider.find_by!(url: params[:url]) + + unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission::MEMBER) + errors << 'Cannot set permission for user' + raise ActiveRecord::Rollback + end + + render json: presented_entity(:server_provider, provider) and return if errors.blank? + + render json: { errors: errors }, status: :unprocessable_entity + end + private def server_provider_params @@ -126,7 +150,7 @@ def server_provider_params end def set_server_provider - @server_provider = ServerProvider.find(params[:id]) + @server_provider = ServerProvider.includes(:server_provider_permissions).find(params[:id]) end def set_provider_credentials(provider, errors) diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 1b47dc3b..7db25ba4 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -71,7 +71,12 @@ def emails end def server_providers - server_providers = current_user.server_providers.includes(:server_provider_permissions).page(params[:page]).per(params[:limit]) + server_providers = current_user.server_providers + .includes(:server_provider_permissions) + .includes(:setting_objects) + .order(:name) + .page(params[:page]) + .per(params[:limit]) render json: paginated_collection(:server_providers, :server_provider, server_providers) end diff --git a/app/serializers/repository_serializer.rb b/app/serializers/repository_serializer.rb index 79705ae3..29673300 100644 --- a/app/serializers/repository_serializer.rb +++ b/app/serializers/repository_serializer.rb @@ -3,7 +3,7 @@ class RepositorySerializer < ApplicationSerializer attributes :id, :name, :url, :token, :last_synced_at, :server_provider_id - attributes(:permission) do |repo, params| - params[:current_user].repository_permission(repo.id).permission + attributes(:permission) do |repo| + repo.repository_permissions&.first&.permission end end diff --git a/app/serializers/server_provider_serializer.rb b/app/serializers/server_provider_serializer.rb index 6427e26a..89052fc4 100644 --- a/app/serializers/server_provider_serializer.rb +++ b/app/serializers/server_provider_serializer.rb @@ -6,6 +6,7 @@ class ServerProviderSerializer < ApplicationSerializer }.freeze PERMISSION = { + nil => '', 1 => 'Owner', 2 => 'User' } @@ -14,5 +15,5 @@ class ServerProviderSerializer < ApplicationSerializer attribute(:type) { |server| PROVIDER_KLASS[server.type.constantize] } attribute(:username) { |server| server.settings(:p4_host).username } - attribute(:permission) { |server, params| PERMISSION[params[:current_user].server_provider_permission(server.id).permission] } + attribute(:permission) { |server, params| PERMISSION[server.server_provider_permissions&.first&.permission] } end diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb index 98986a10..83f30cbf 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.erb +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -2,7 +2,7 @@

Someone has requested a link to change your password. You can do this through the link below.

-

<%= link_to 'Change my password', URI.join(Settings.web_url, "setup_new_password?reset_password_token=#{@token}" %>

+

<%= link_to 'Change my password', URI.join(Settings.web_url, "setup_new_password?reset_password_token=#{@token}") %>

If you didn't request this, please ignore this email.

Your password won't change until you access the link above and create a new one.

diff --git a/config/routes.rb b/config/routes.rb index 719b23d4..32cf3591 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -45,6 +45,7 @@ resources :server_providers, only: [:create, :show, :update] do collection do get :by_url + post :add_by_url end member do From aa78160b794fd1f7f6df08e15efe346bd1eb6e94 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Wed, 28 Jul 2021 16:25:24 +0300 Subject: [PATCH 28/65] Feedback mailer --- .../v1/users/registrations_controller.rb | 5 +++++ app/mailers/feedback_mailer.rb | 10 ++++++++++ .../reset_password_instructions.html.erb | 2 +- .../feedback_mailer/send_feedback.html.erb | 20 +++++++++++++++++++ .../feedback_mailer/send_feedback.text.erb | 18 +++++++++++++++++ app/views/layouts/mailer.html.erb | 13 ++++++++++++ app/views/layouts/mailer.text.erb | 1 + config/settings.yml | 3 ++- config/settings/development.yml | 5 +++-- 9 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 app/mailers/feedback_mailer.rb create mode 100644 app/views/feedback_mailer/send_feedback.html.erb create mode 100644 app/views/feedback_mailer/send_feedback.text.erb create mode 100644 app/views/layouts/mailer.html.erb create mode 100644 app/views/layouts/mailer.text.erb diff --git a/app/controllers/v1/users/registrations_controller.rb b/app/controllers/v1/users/registrations_controller.rb index f66ebdcb..7110cd57 100644 --- a/app/controllers/v1/users/registrations_controller.rb +++ b/app/controllers/v1/users/registrations_controller.rb @@ -25,6 +25,11 @@ def update def destroy render json: { errors: [ 'Invalid credentials' ] }, status: :unprocessable_entity and return unless resource.valid_password?(params[:password]) + if params[:feedback].present? + permitted = params.require(:feedback).permit(:reason, :text) + FeedbackMailer.with(email: resource.email, feedback: permitted).send_feedback.deliver_now + end + resource.mark_as_deleted sign_out head :ok diff --git a/app/mailers/feedback_mailer.rb b/app/mailers/feedback_mailer.rb new file mode 100644 index 00000000..9df73df2 --- /dev/null +++ b/app/mailers/feedback_mailer.rb @@ -0,0 +1,10 @@ +class FeedbackMailer < ApplicationMailer + default from: 'from@example.com' + layout 'mailer' + + def send_feedback + @email = params[:email] + @feedback = params[:feedback] + mail(to: Settings.feedback_mail, subject: 'Account removed') + end +end \ No newline at end of file diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb index 83f30cbf..4cbe7044 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.erb +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -2,7 +2,7 @@

Someone has requested a link to change your password. You can do this through the link below.

-

<%= link_to 'Change my password', URI.join(Settings.web_url, "setup_new_password?reset_password_token=#{@token}") %>

+

<%= link_to 'Change my password', URI.join(Settings.web_url, "setup_new_password?reset_password_token=#{@token}").to_s %>

If you didn't request this, please ignore this email.

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/feedback_mailer/send_feedback.html.erb b/app/views/feedback_mailer/send_feedback.html.erb new file mode 100644 index 00000000..8d74c390 --- /dev/null +++ b/app/views/feedback_mailer/send_feedback.html.erb @@ -0,0 +1,20 @@ +The account <%= @email %> was removed. +

+<% if @feedback[:reason].present? %> +The reason was <%= @feedback[:reason] %> +<% end -%> +

+<% if @feedback[:text].present? %> +The customer provided the following feedback: +

+--- +<%= @feedback[:text] %> +--- + +<% else %> +The customer provided no further feedback. +<% end %> +

+Best regards, +
+Your Travis CI VCS Proxy Bot diff --git a/app/views/feedback_mailer/send_feedback.text.erb b/app/views/feedback_mailer/send_feedback.text.erb new file mode 100644 index 00000000..ea383ca8 --- /dev/null +++ b/app/views/feedback_mailer/send_feedback.text.erb @@ -0,0 +1,18 @@ +The account <%= @email %> was removed. + +<% if @feedback[:reason].present? %> +The reason was <%= @feedback[:reason] %> +<% end -%> + +<% if @feedback[:text].present? %> +The customer provided the following feedback: + +--- +<%= @feedback[:text] %> +--- +<% else %> +The customer provided no further feedback. +<% end %> + +Best regards, +Your Travis CI VCS Proxy Bot diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 00000000..cbd34d2e --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 00000000..cd9bb66d --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> \ No newline at end of file diff --git a/config/settings.yml b/config/settings.yml index c626dc66..925af18e 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -3,4 +3,5 @@ jwt: otp_secret_encryption_key: 12391230-12e90-2if912d120s912is0921iw9012iw9012wi1290wi1290f139 p4_token_encryption_key: HTmcYwLgONSdedIPBrUcsVQjJjQkFZBE web_url: https://travis-vcs-proxy.travis-ci.org -api_url: https://travis-vcs-proxy-api.travis-ci.org \ No newline at end of file +api_url: https://travis-vcs-proxy-api.travis-ci.org +feedback_mail: 'cancellations@travis-ci.com' \ No newline at end of file diff --git a/config/settings/development.yml b/config/settings/development.yml index b06c73c2..8995de07 100644 --- a/config/settings/development.yml +++ b/config/settings/development.yml @@ -1,2 +1,3 @@ -web_url: http://localhost:13007 -api_url: http://localhost:13006 \ No newline at end of file +web_url: http://192.168.1.161:13007 +api_url: http://192.168.1.161:13006 +feedback_mail: example@example.com \ No newline at end of file From 301808c6b46b71a6cbafebc7385ab515f2069320 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Thu, 29 Jul 2021 13:27:15 +0300 Subject: [PATCH 29/65] Assign deleted email --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index a32516ea..44792b65 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -36,7 +36,7 @@ def repository_permission(repository_id) end def mark_as_deleted - update_columns(email: '', name: nil, active: false) + update_columns(email: "deleted_email_#{Kernel.rand(1000000000)}", name: nil, active: false) end private From 0cb27a41cb53c4502358a89d3bae370e8cdc1c3b Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Thu, 29 Jul 2021 17:03:50 +0300 Subject: [PATCH 30/65] Add listener endpoint --- app/controllers/v1/webhooks_controller.rb | 23 +++++++++++++++++++++++ app/models/commit.rb | 9 +++++++++ app/models/ref.rb | 9 +++++---- config/routes.rb | 2 ++ lib/travis/vcs_proxy/p4_connection.rb | 16 ++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 app/controllers/v1/webhooks_controller.rb create mode 100644 app/models/commit.rb diff --git a/app/controllers/v1/webhooks_controller.rb b/app/controllers/v1/webhooks_controller.rb new file mode 100644 index 00000000..11514ee7 --- /dev/null +++ b/app/controllers/v1/webhooks_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class V1::WebhooksController < ApplicationController + def receive + head :unauthorized and return unless server_provider = ServerProvider.find_by(listener_token: params[:token]) + + connection = Travis::VcsProxy::P4Connection.new(server_provider.url, server_provider.settings(:p4_host).username, server_provider.token) + head :internal_server_error unless commit_info = connection.commit_info(params[:change_root], params[:username]) + + # TODO: Figure out if we should really ignore the hook if there is no user with the given email + head :ok and return unless user = server_provider.users.find_by(email: commit_info[:email]) + # TODO: Figure out if we should really ignore the hook if there is no repository with this name + head :ok and return unless repository = server_provider.repositories.find_by(name: commit_info[:repository_name]) + + ref = repository.refs.branch.find_by(name: commit_info[:ref]) || repository.refs.branch.create(name: commit_info[:ref]) + head :unprocessable_entity and return unless ref + + commit = ref.commits.find_by(sha: params[:sha]) || ref.commits.create(sha: params[:sha], repository: repository, user: user) + head :unprocessable_entity and return unless commit + + head :ok + end +end diff --git a/app/models/commit.rb b/app/models/commit.rb new file mode 100644 index 00000000..8b1cba1d --- /dev/null +++ b/app/models/commit.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Commit < ApplicationRecord + belongs_to :ref + belongs_to :repository + belongs_to :user + + validates_presence_of :ref_id, :repository_id, :user_id, :sha +end \ No newline at end of file diff --git a/app/models/ref.rb b/app/models/ref.rb index 7786b177..8ee1d929 100644 --- a/app/models/ref.rb +++ b/app/models/ref.rb @@ -1,15 +1,16 @@ # frozen_string_literal: true class Ref < ApplicationRecord - BRANCH = 1 - TAG = 2 + enum type: [:branch, :tag] self.inheritance_column = nil belongs_to :repository + has_many :commits, dependent: :destroy + validates_presence_of :name, :type, :repository_id - scope :branch, -> { where(type: BRANCH) } - scope :tag, -> { where(type: TAG) } + scope :branch, -> { where(type: types[:branch]) } + scope :tag, -> { where(type: types[:tag]) } end diff --git a/config/routes.rb b/config/routes.rb index 32cf3591..7a9187af 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,8 @@ Rails.application.routes.draw do root to: 'home#index' + post :listener, to: 'v1/webhooks#receive' + scope :v1, module: :v1 do devise_for :users, controllers: { diff --git a/lib/travis/vcs_proxy/p4_connection.rb b/lib/travis/vcs_proxy/p4_connection.rb index c8b311bb..e41cd65c 100644 --- a/lib/travis/vcs_proxy/p4_connection.rb +++ b/lib/travis/vcs_proxy/p4_connection.rb @@ -65,6 +65,22 @@ def file_contents(repository_name, ref, path) p4.run_print("//#{repository_name}/#{ref}/#{path}") rescue P4Exception => e puts e.message.inspect + + nil + end + + def commit_info(change_root, username) + matches = change_root.match(/\A\/\/([^\/]+)\/([^\/]+)/) + return if matches.nil? + + { + repository_name: matches[1], + ref: matches[2], + email: p4.run_user('-o', username).first['Email'].encode('utf-8'), + } + rescue P4Exception => e + puts e.message.inspect + nil end From b3a6d09815ae3ceeea3486ecd3d7a1a25f77d5cd Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Tue, 3 Aug 2021 10:48:10 +0300 Subject: [PATCH 31/65] Refactor P4-specific code to a more generic one --- .../v1/repositories/token_controller.rb | 13 +- app/controllers/v1/repositories_controller.rb | 4 +- .../v1/server_providers_controller.rb | 23 +--- app/controllers/v1/webhooks_controller.rb | 3 +- app/models/p4_server_provider.rb | 24 ++++ app/models/repository.rb | 8 ++ app/models/server_provider.rb | 12 ++ .../authenticate_user_with_server_provider.rb | 31 +++++ app/services/update_repository_credentials.rb | 33 +++++ config/settings/development.yml | 6 +- lib/travis/vcs_proxy/p4_connection.rb | 113 ----------------- lib/travis/vcs_proxy/repositories/p4.rb | 116 ++++++++++++++++++ lib/travis/vcs_proxy/sync/repository.rb | 8 +- lib/travis/vcs_proxy/sync/server_provider.rb | 19 +-- 14 files changed, 246 insertions(+), 167 deletions(-) create mode 100644 app/services/authenticate_user_with_server_provider.rb create mode 100644 app/services/update_repository_credentials.rb delete mode 100644 lib/travis/vcs_proxy/p4_connection.rb create mode 100644 lib/travis/vcs_proxy/repositories/p4.rb diff --git a/app/controllers/v1/repositories/token_controller.rb b/app/controllers/v1/repositories/token_controller.rb index e2b048b5..db7e2fc0 100644 --- a/app/controllers/v1/repositories/token_controller.rb +++ b/app/controllers/v1/repositories/token_controller.rb @@ -10,15 +10,14 @@ def update permission = current_user.repository_permission(@repository.id) head :forbidden and return if permission.blank? || (!permission.owner? && !permission.admin?) + success = false begin - ValidateP4Credentials.new(params[:username], params[:token], @repository.server_provider.url).call - rescue ValidateP4Credentials::ValidationFailed + success = UpdateRepositoryCredentials.new(@repository, params[:username], params[:token]).call + rescue UpdateRepositoryCredentials::ValidationFailed render json: { errors: [ 'Cannot authenticate' ] }, status: :unprocessable_entity and return end - @repository.settings(:p4_host).username = params[:username] - @repository.token = params[:token] - head :ok and return if @repository.save + head :ok and return if success render json: { errors: @repository.errors }, status: :unprocessable_entity end @@ -27,9 +26,7 @@ def destroy permission = current_user.repository_permission(@repository.id) head :forbidden and return if permission.blank? || (!permission.owner? && !permission.admin?) - @repository.settings(:p4_host).username = nil - @repository.token = nil - head :ok and return if @repository.save + head :ok and return if UpdateRepositoryCredentials.new(@repository, nil, nil).call render json: { errors: @repository.errors }, status: :unprocessable_entity end diff --git a/app/controllers/v1/repositories_controller.rb b/app/controllers/v1/repositories_controller.rb index 285f15d0..f57c6d1d 100644 --- a/app/controllers/v1/repositories_controller.rb +++ b/app/controllers/v1/repositories_controller.rb @@ -15,9 +15,7 @@ def refs def content head :bad_request and return if params[:ref].blank? || params[:path].blank? - connection = Travis::VcsProxy::P4Connection.new(@repository.server_provider.url, @repository.server_provider.settings(:p4_host).username, @repository.server_provider.token) - - result = connection.file_contents(@repository.name, params[:ref], params[:path]) + result = @repository.file_contents(params[:ref], params[:path]) render json: { errors: [ 'Cannot render file' ] }, status: :unprocessable_entity and return if result.blank? render plain: result[1] diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index c894dfb3..cbdaec7d 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -75,17 +75,7 @@ def authenticate end end - begin - ValidateP4Credentials.new(params[:username], params[:token], @server_provider.url).call - rescue ValidateP4Credentials::ValidationFailed => e - success = false - raise ActiveRecord::Rollback - end - - setting = permission.setting || permission.build_setting - setting.token = params[:token] - setting.username = params[:username] - unless setting.save + unless AuthenticateUserWithServerProvider.new(permission, @server_provider, params[:username], params[:token]).call success = false raise ActiveRecord::Rollback end @@ -156,18 +146,15 @@ def set_server_provider def set_provider_credentials(provider, errors) return if params[:server_provider][:username].blank? || params[:server_provider][:token].blank? + success = false begin - ValidateP4Credentials.new(params[:server_provider][:username], params[:server_provider][:token], provider.url).call - rescue ValidateP4Credentials::ValidationFailed => e - success = false + success = UpdateRepositoryCredentials.new(provider, params[:server_provider][:username], params[:server_provider][:token]) + rescue UpdateRepositoryCredentials::ValidationFailed => e errors << 'Cannot authenticate' raise ActiveRecord::Rollback end - provider.settings(:p4_host).username = params[:server_provider][:username] - provider.token = params[:server_provider][:token] - unless provider.save - success = false + unless success errors << 'Cannot save credentials' raise ActiveRecord::Rollback end diff --git a/app/controllers/v1/webhooks_controller.rb b/app/controllers/v1/webhooks_controller.rb index 11514ee7..a826db39 100644 --- a/app/controllers/v1/webhooks_controller.rb +++ b/app/controllers/v1/webhooks_controller.rb @@ -4,8 +4,7 @@ class V1::WebhooksController < ApplicationController def receive head :unauthorized and return unless server_provider = ServerProvider.find_by(listener_token: params[:token]) - connection = Travis::VcsProxy::P4Connection.new(server_provider.url, server_provider.settings(:p4_host).username, server_provider.token) - head :internal_server_error unless commit_info = connection.commit_info(params[:change_root], params[:username]) + head :internal_server_error unless commit_info = server_provider.commit_info_from_webhook(params) # TODO: Figure out if we should really ignore the hook if there is no user with the given email head :ok and return unless user = server_provider.users.find_by(email: commit_info[:email]) diff --git a/app/models/p4_server_provider.rb b/app/models/p4_server_provider.rb index 04bf7751..f22797cb 100644 --- a/app/models/p4_server_provider.rb +++ b/app/models/p4_server_provider.rb @@ -3,4 +3,28 @@ class P4ServerProvider < ServerProvider include P4HostSettings include EncryptedToken + + def bare_repo(repository = nil, username = nil, password = nil) + if username.present? && password.present? + repo_token = password + elsif repository.present? && repository.settings(:p4_host).username.present? + username = repository.settings(:p4_host).username + repo_token = repository.token + else + username = settings(:p4_host).username + repo_token = token + end + + Travis::VcsProxy::Repositories::P4.new(repository, url, username, repo_token) + end + + def remote_repositories(username = nil, token = nil) + bare_repo(nil, username, token).repositories + end + + def commit_info_from_webhook(payload) + return unless payload.key?(:change_root) && payload.key?(:username) + + bare_repo.commit_info(payload[:change_root], payload[:username]) + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index a1051668..21ca72c3 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -20,4 +20,12 @@ def branches def tags refs.tag end + + def repo + server_provider.bare_repo(self) + end + + def file_contents(ref, path) + repo.file_contents(ref, path) + end end diff --git a/app/models/server_provider.rb b/app/models/server_provider.rb index 7942494f..7d7a5080 100644 --- a/app/models/server_provider.rb +++ b/app/models/server_provider.rb @@ -14,6 +14,18 @@ class ServerProvider < ApplicationRecord before_save :generate_listener_token + def bare_repo(*args) + raise NotImplementedError + end + + def remote_repositories + raise NotImplementedError + end + + def commit_info_from_webhook(payload) + raise NotImplementedError + end + private def generate_listener_token diff --git a/app/services/authenticate_user_with_server_provider.rb b/app/services/authenticate_user_with_server_provider.rb new file mode 100644 index 00000000..7c0d9bfc --- /dev/null +++ b/app/services/authenticate_user_with_server_provider.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class AuthenticateUserWithServerProvider + def initialize(server_provider_permission, server_provider, username, password) + @server_provider_permission = server_provider_permission + @server_provider = server_provider + @username = username + @password = password + end + + def call + case @server_provider + when P4ServerProvider then authenticate_p4 + end + end + + private + + def authenticate_p4 + begin + ValidateP4Credentials.new(@username, @password, @server_provider.url).call + rescue ValidateP4Credentials::ValidationFailed => e + return false + end + + setting = @server_provider_permission.setting || @server_provider_permission.build_setting + setting.token = @token + setting.username = @username + setting.save + end +end \ No newline at end of file diff --git a/app/services/update_repository_credentials.rb b/app/services/update_repository_credentials.rb new file mode 100644 index 00000000..49156e80 --- /dev/null +++ b/app/services/update_repository_credentials.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class UpdateRepositoryCredentials + class ValidationFailed < StandardError; end + + def initialize(entity, username, password) + @entity = entity + @username = username + @password = password + end + + def call + server_provider = case @entity + when Repository then @entity.server_provider + when ServerProvider then @entity + end + + case server_provider + when P4ServerProvider + if @username.present? || @password.present? + begin + ValidateP4Credentials.new(@username, @password, server_provider.url).call + rescue ValidateP4Credentials::ValidationFailed + raise ValidationFailed + end + end + + @entity.settings(:p4_host).username = @username + @entity.token = @password + @entity.save + end + end +end \ No newline at end of file diff --git a/config/settings/development.yml b/config/settings/development.yml index 8995de07..b2d23aac 100644 --- a/config/settings/development.yml +++ b/config/settings/development.yml @@ -1,3 +1,3 @@ -web_url: http://192.168.1.161:13007 -api_url: http://192.168.1.161:13006 -feedback_mail: example@example.com \ No newline at end of file +web_url: http://localhost:13007 +api_url: http://localhost:13006 +feedback_mail: example@example.com diff --git a/lib/travis/vcs_proxy/p4_connection.rb b/lib/travis/vcs_proxy/p4_connection.rb deleted file mode 100644 index e41cd65c..00000000 --- a/lib/travis/vcs_proxy/p4_connection.rb +++ /dev/null @@ -1,113 +0,0 @@ -# frozen_string_literal: true -require 'P4' - -module Travis - module VcsProxy - class P4Connection - class PermissionNotFound < StandardError; end - - def initialize(url, username, token) - @url = url - @username = username - @token = token - end - - def repositories - @repositories ||= p4.run_depots.map do |depot| - { - name: depot['name'] - } - end - rescue P4Exception => e - puts e.message.inspect - - [] - end - - def branches(repository_name) - @branches ||= p4.run_streams("//#{repository_name}/...").map do |stream| - { - name: stream['Stream'] - } - end - rescue P4Exception => e - puts e.message.inspect - - [] - end - - def users - @users ||= p4.run_users('-a').map do |user| - next unless user['Email'].include?('@') - - { - email: user['Email'], - name: user['User'] - } - end.compact - rescue P4Exception => e - puts e.message.inspect - - [] - end - - def permissions(repository_name) - @permissions ||= users.each_with_object({}) do |user, memo| - memo[user[:email]] = p4.run_protects('-u', user[:name], '-M', "//#{repository_name}/...").first['permMax'] - end - rescue P4Exception => e - puts e.message.inspect - - {} - end - - def file_contents(repository_name, ref, path) - p4.run_print("//#{repository_name}/#{ref}/#{path}") - rescue P4Exception => e - puts e.message.inspect - - nil - end - - def commit_info(change_root, username) - matches = change_root.match(/\A\/\/([^\/]+)\/([^\/]+)/) - return if matches.nil? - - { - repository_name: matches[1], - ref: matches[2], - email: p4.run_user('-o', username).first['Email'].encode('utf-8'), - } - rescue P4Exception => e - puts e.message.inspect - - nil - end - - private - - def p4 - return @p4 if defined?(@p4) - - file = Tempfile.new('p4ticket') - file.write(@token) - file.close - - p4 = P4.new - p4.charset = 'utf8' - p4.port = @url - p4.user = @username - p4.connect - p4.run_login - - p4 - rescue P4Exception => e - puts e.message.inspect - raise - ensure - file.unlink if file - ENV.delete('P4TICKETS') - end - end - end -end diff --git a/lib/travis/vcs_proxy/repositories/p4.rb b/lib/travis/vcs_proxy/repositories/p4.rb new file mode 100644 index 00000000..72b2eaec --- /dev/null +++ b/lib/travis/vcs_proxy/repositories/p4.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true +require 'P4' + +module Travis + module VcsProxy + module Repositories + class P4 + class PermissionNotFound < StandardError; end + + def initialize(repository, url, username, token) + @repository = repository + @url = url + @username = username + @token = token + end + + def repositories + @repositories ||= p4.run_depots.map do |depot| + { + name: depot['name'] + } + end + rescue P4Exception => e + puts e.message.inspect + + [] + end + + def branches + @branches ||= p4.run_streams("//#{@repository.name}/...").map do |stream| + { + name: stream['Stream'] + } + end + rescue P4Exception => e + puts e.message.inspect + + [] + end + + def users + @users ||= p4.run_users('-a').map do |user| + next unless user['Email'].include?('@') + + { + email: user['Email'], + name: user['User'] + } + end.compact + rescue P4Exception => e + puts e.message.inspect + + [] + end + + def permissions + @permissions ||= users.each_with_object({}) do |user, memo| + memo[user[:email]] = p4.run_protects('-u', user[:name], '-M', "//#{@repository.name}/...").first['permMax'] + end + rescue P4Exception => e + puts e.message.inspect + + {} + end + + def file_contents(ref, path) + p4.run_print("//#{@repository.name}/#{ref}/#{path}") + rescue P4Exception => e + puts e.message.inspect + + nil + end + + def commit_info(change_root, username) + matches = change_root.match(/\A\/\/([^\/]+)\/([^\/]+)/) + return if matches.nil? + + { + repository_name: matches[1], + ref: matches[2], + email: p4.run_user('-o', username).first['Email'].encode('utf-8'), + } + rescue P4Exception => e + puts e.message.inspect + + nil + end + + private + + def p4 + return @p4 if defined?(@p4) + + file = Tempfile.new('p4ticket') + file.write(@token) + file.close + + @p4 = ::P4.new + @p4.charset = 'utf8' + @p4.port = @url + @p4.user = @username + @p4.connect + @p4.run_login + + @p4 + rescue P4Exception => e + puts e.message.inspect + raise + ensure + file.unlink if file + ENV.delete('P4TICKETS') + end + end + end + end +end diff --git a/lib/travis/vcs_proxy/sync/repository.rb b/lib/travis/vcs_proxy/sync/repository.rb index 0944beb9..4132adec 100644 --- a/lib/travis/vcs_proxy/sync/repository.rb +++ b/lib/travis/vcs_proxy/sync/repository.rb @@ -21,13 +21,11 @@ def sync end return if username.blank? || token.blank? - connection = Travis::VcsProxy::P4Connection.new(@repository.server_provider.url, username, token) - connection.branches(@repository.name).each do |branch| - @repository.refs.find_or_create_by!(type: Ref::BRANCH, name: branch[:name].sub(/\A\/\/#{Regexp.escape(@repository.name)}\//, '')) + @repository.repo.branches.each do |branch| + @repository.branches.find_or_create_by!(name: branch[:name].sub(/\A\/\/#{Regexp.escape(@repository.name)}\//, '')) end - perms = connection.permissions(@repository.name) - Sidekiq.logger.debug "PERMS: #{perms.inspect}" + perms = @repository.repo.permissions return if perms.blank? repo_emails = perms.keys users = ::User.where(email: repo_emails).group_by(&:email) diff --git a/lib/travis/vcs_proxy/sync/server_provider.rb b/lib/travis/vcs_proxy/sync/server_provider.rb index def7fe35..02ca3ccf 100644 --- a/lib/travis/vcs_proxy/sync/server_provider.rb +++ b/lib/travis/vcs_proxy/sync/server_provider.rb @@ -11,36 +11,25 @@ def initialize(server_provider, user) def sync ActiveRecord::Base.transaction do - server_provider_username = @server_provider.settings(:p4_host).username - server_provider_token = @server_provider.token - if server_provider_username.present? && server_provider_token.present? - connection = Travis::VcsProxy::P4Connection.new(@server_provider.url, server_provider_username, server_provider_token) - sync_connection_data(connection) - return - end + sync_repositories(@server_provider.remote_repositories) @server_provider.users.each do |user| next unless permission_setting = user.server_provider_permission(@server_provider.id).setting - connection = Travis::VcsProxy::P4Connection.new(@server_provider.url, permission_setting.username, permission_setting.token) - sync_connection_data(connection) + sync_repositories(@server_provider.remote_repositories(permission_setting.username, permission_setting.token)) end end end private - def sync_connection_data(connection) - repos = connection.repositories.map do |repository| + def sync_repositories(repositories) + repositories.each do |repository| repo = @server_provider.repositories.find_or_initialize_by(name: repository[:name]) unless repo.persisted? repo.url = 'STUB' repo.save! end - repo - end - - repos.each do |repo| Travis::VcsProxy::Sync::Repository.new(repo, @user).sync end end From 95a94293955e36fe24f272b4969980f71702a6cc Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Thu, 19 Aug 2021 16:02:27 +0300 Subject: [PATCH 32/65] Implement a production-ready Dockerfile --- Dockerfile | 18 ++++++++++++------ Dockerfile.dev | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 Dockerfile.dev diff --git a/Dockerfile b/Dockerfile index 3488405f..952395f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,11 +7,17 @@ RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ postgresql-client nodejs yarn \ && rm -rf /var/lib/apt/lists/* -WORKDIR /srv/app -# COPY Gemfile* ./ -# RUN bundle install -# COPY . . +RUN bundle config --global frozen 1 +RUN bundle config set deployment 'true' +RUN bundle config set without 'development test' + +WORKDIR /app +COPY Gemfile* ./ + +RUN gem install bundler -v '2.1.4' +RUN bundle install + +COPY . . EXPOSE 3000 -ENTRYPOINT ["/srv/app/entrypoint.sh"] -CMD ["rails", "server", "-b", "0.0.0.0"] +CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 00000000..3488405f --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,17 @@ +FROM ruby:3.0.1 + +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ + curl -sL https://deb.nodesource.com/setup_16.x | bash -s && \ + apt-get install -y --no-install-recommends \ + postgresql-client nodejs yarn \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /srv/app +# COPY Gemfile* ./ +# RUN bundle install +# COPY . . + +EXPOSE 3000 +ENTRYPOINT ["/srv/app/entrypoint.sh"] +CMD ["rails", "server", "-b", "0.0.0.0"] From b68dcf4f2faedb25eb9c3e3162d245721041b054 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Wed, 25 Aug 2021 09:53:21 +0300 Subject: [PATCH 33/65] Fix sync with Assembla repos --- .../authenticate_user_with_server_provider.rb | 2 +- app/services/validate_p4_credentials.rb | 3 ++- lib/travis/vcs_proxy/repositories/p4.rb | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/services/authenticate_user_with_server_provider.rb b/app/services/authenticate_user_with_server_provider.rb index 7c0d9bfc..287d709a 100644 --- a/app/services/authenticate_user_with_server_provider.rb +++ b/app/services/authenticate_user_with_server_provider.rb @@ -24,7 +24,7 @@ def authenticate_p4 end setting = @server_provider_permission.setting || @server_provider_permission.build_setting - setting.token = @token + setting.token = @password setting.username = @username setting.save end diff --git a/app/services/validate_p4_credentials.rb b/app/services/validate_p4_credentials.rb index 26628d1b..5498d9a6 100644 --- a/app/services/validate_p4_credentials.rb +++ b/app/services/validate_p4_credentials.rb @@ -28,7 +28,8 @@ def call p4.port = @url p4.user = @username p4.connect - p4.run_login + p4.run_trust('-y') + p4.run_protects nil rescue P4Exception => e diff --git a/lib/travis/vcs_proxy/repositories/p4.rb b/lib/travis/vcs_proxy/repositories/p4.rb index 72b2eaec..8d25cd77 100644 --- a/lib/travis/vcs_proxy/repositories/p4.rb +++ b/lib/travis/vcs_proxy/repositories/p4.rb @@ -95,19 +95,29 @@ def p4 file.write(@token) file.close + ENV['P4TICKETS'] = file.path + @p4 = ::P4.new @p4.charset = 'utf8' @p4.port = @url @p4.user = @username @p4.connect - @p4.run_login + @p4.run_trust('-y') + @p4.run_protects @p4 rescue P4Exception => e puts e.message.inspect raise ensure - file.unlink if file + if file + begin + file.unlink + file.close + rescue + end + end + ENV.delete('P4TICKETS') end end From f88f1fa6c8c24eb345d677f546114a5ca466e005 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 27 Aug 2021 15:19:05 +0300 Subject: [PATCH 34/65] Setup Config gem --- config/initializers/config.rb | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 config/initializers/config.rb diff --git a/config/initializers/config.rb b/config/initializers/config.rb new file mode 100644 index 00000000..da32bc9f --- /dev/null +++ b/config/initializers/config.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +Config.setup do |config| + config.use_env = true + config.env_prefix = 'SETTINGS' + config.env_separator = '__' +end From 2f197baadd313cb75641efe5fee8881aff39f739 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 27 Aug 2021 16:00:36 +0300 Subject: [PATCH 35/65] Tweak database.yml to accept ENV variables --- config/database.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/database.yml b/config/database.yml index 03c07209..89517814 100644 --- a/config/database.yml +++ b/config/database.yml @@ -3,8 +3,8 @@ default: &default encoding: unicode pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> host: <%= ENV['DB_HOST'] || 'localhost' %> - user: root - password: root + user: <%= ENV['DB_USER'] || 'root' %> + password: <%= ENV['DB_PASSWORD'] || 'root' %> development: <<: *default @@ -16,4 +16,4 @@ test: production: <<: *default - database: travis_vcs_proxy_production + database: <%= ENV['DB_NAME'] || 'travis_vcs_proxy_production' %> From 7f982e427ff4c770d40da35634731872afa1a41c Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 27 Aug 2021 16:46:17 +0300 Subject: [PATCH 36/65] Add SMTP ENV vars --- config/environments/production.rb | 10 ++++++++++ config/initializers/000_config.rb | 4 ++-- config/initializers/config.rb | 7 ------- 3 files changed, 12 insertions(+), 9 deletions(-) delete mode 100644 config/initializers/config.rb diff --git a/config/environments/production.rb b/config/environments/production.rb index 01ecc946..3ffb249e 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -100,4 +100,14 @@ # config.active_record.database_selector = { delay: 2.seconds } # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + + config.action_mailer.delivery_method = :smtp + config.action_mailer.smtp_settings = { + address: ENV['SMTP_HOST'], + port: ENV['SMTP_PORT'], + user_name: ENV['SMTP_USER'], + password: ENV['SMTP_PASSWORD'], + authentication: 'plain', + enable_starttls_auto: true + } end diff --git a/config/initializers/000_config.rb b/config/initializers/000_config.rb index 8c4347a9..21ff742b 100644 --- a/config/initializers/000_config.rb +++ b/config/initializers/000_config.rb @@ -17,7 +17,7 @@ # Load environment variables from the `ENV` object and override any settings defined in files. # - # config.use_env = false + config.use_env = true # Define ENV variable prefix deciding which variables to load into config. # @@ -32,7 +32,7 @@ # with Heroku, but you might want to change it for example for '__' to easy override settings from command line, where # using dots in variable names might not be allowed (eg. Bash). # - # config.env_separator = '.' + config.env_separator = '__' # Ability to process variables names: # * nil - no change diff --git a/config/initializers/config.rb b/config/initializers/config.rb deleted file mode 100644 index da32bc9f..00000000 --- a/config/initializers/config.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -Config.setup do |config| - config.use_env = true - config.env_prefix = 'SETTINGS' - config.env_separator = '__' -end From 6a90acb361b234cc4238b499d29f0241c865727d Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 27 Aug 2021 16:56:31 +0300 Subject: [PATCH 37/65] Add SMTP domain --- config/environments/production.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/environments/production.rb b/config/environments/production.rb index 3ffb249e..e4c43064 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -107,6 +107,7 @@ port: ENV['SMTP_PORT'], user_name: ENV['SMTP_USER'], password: ENV['SMTP_PASSWORD'], + domain: ENV['SMTP_DOMAIN'], authentication: 'plain', enable_starttls_auto: true } From d7c1762025d425c3ad9468d4ac54c25385228cfb Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 27 Aug 2021 17:13:58 +0300 Subject: [PATCH 38/65] Rename config initializer --- config/initializers/{000_config.rb => config.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename config/initializers/{000_config.rb => config.rb} (100%) diff --git a/config/initializers/000_config.rb b/config/initializers/config.rb similarity index 100% rename from config/initializers/000_config.rb rename to config/initializers/config.rb From 4358901c8df61848023ff18dac025da96d6c8a74 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Mon, 30 Aug 2021 14:44:31 +0300 Subject: [PATCH 39/65] Set up mail sender --- app/mailers/application_mailer.rb | 2 +- config/initializers/devise.rb | 2 +- config/settings.yml | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 286b2239..f78416ca 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,4 @@ class ApplicationMailer < ActionMailer::Base - default from: 'from@example.com' + default from: Settings.mail_from layout 'mailer' end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 10913826..6807160b 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -28,7 +28,7 @@ # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. - config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + config.mailer_sender = Settings.mail_from # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' diff --git a/config/settings.yml b/config/settings.yml index 925af18e..a58eddd9 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -4,4 +4,5 @@ otp_secret_encryption_key: 12391230-12e90-2if912d120s912is0921iw9012iw9012wi1290 p4_token_encryption_key: HTmcYwLgONSdedIPBrUcsVQjJjQkFZBE web_url: https://travis-vcs-proxy.travis-ci.org api_url: https://travis-vcs-proxy-api.travis-ci.org -feedback_mail: 'cancellations@travis-ci.com' \ No newline at end of file +feedback_mail: 'cancellations@travis-ci.com' +mail_from: test@example.com From 8a955b782d1dd90a44e354da802127851d2056dc Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Wed, 18 Aug 2021 15:43:29 +0300 Subject: [PATCH 40/65] Add OAuth server via doorkeeper Also: * add commits and webhooks APIs * trigger webhook when a push event is triggered via webhook listener --- Gemfile | 2 + Gemfile.lock | 8 + app/controllers/application_controller.rb | 19 + .../v1/repositories/commits_controller.rb | 30 ++ .../v1/repositories/webhooks_controller.rb | 48 ++ app/controllers/v1/repositories_controller.rb | 2 +- .../v1/server_providers_controller.rb | 12 +- app/controllers/v1/webhooks_controller.rb | 2 + app/jobs/webhook_trigger_job.rb | 9 + app/models/p4_server_provider.rb | 8 + app/models/repository.rb | 5 +- app/models/server_provider.rb | 8 + app/models/server_provider_permission.rb | 7 +- app/models/user.rb | 10 + app/models/webhook.rb | 10 + app/serializers/commit_serializer.rb | 12 + app/serializers/ref_serializer.rb | 6 + app/serializers/repository_serializer.rb | 11 +- app/serializers/user_serializer.rb | 1 + app/serializers/webhook_serializer.rb | 5 + app/services/trigger_webhooks.rb | 49 ++ config/environments/development.rb | 2 + config/initializers/doorkeeper.rb | 490 ++++++++++++++++++ config/locales/doorkeeper.en.yml | 148 ++++++ config/routes.rb | 8 + lib/travis/vcs_proxy/repositories/p4.rb | 18 + lib/travis/vcs_proxy/sync/repository.rb | 48 +- spec/models/webhook_spec.rb | 5 + 28 files changed, 946 insertions(+), 37 deletions(-) create mode 100644 app/controllers/v1/repositories/commits_controller.rb create mode 100644 app/controllers/v1/repositories/webhooks_controller.rb create mode 100644 app/jobs/webhook_trigger_job.rb create mode 100644 app/models/webhook.rb create mode 100644 app/serializers/commit_serializer.rb create mode 100644 app/serializers/webhook_serializer.rb create mode 100644 app/services/trigger_webhooks.rb create mode 100644 config/initializers/doorkeeper.rb create mode 100644 config/locales/doorkeeper.en.yml create mode 100644 spec/models/webhook_spec.rb diff --git a/Gemfile b/Gemfile index 57c93812..26bd4342 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ gem 'rails', '~> 6.1.3', '>= 6.1.3.2' gem 'devise' gem 'devise-jwt' gem 'devise-two-factor' +gem 'doorkeeper' gem 'ledermann-rails-settings' gem 'pg' gem 'redis' @@ -17,6 +18,7 @@ gem 'jsonapi-serializer' gem 'p4ruby' gem 'sidekiq' gem 'kaminari' +gem 'validate_url' gem 'bootsnap', require: false diff --git a/Gemfile.lock b/Gemfile.lock index bf113d92..90ffe2ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,6 +92,8 @@ GEM railties (< 6.2) rotp (~> 6.0) diff-lcs (1.4.4) + doorkeeper (5.5.2) + railties (>= 5) dry-auto_inject (0.8.0) dry-container (>= 0.3.4) dry-configurable (0.12.1) @@ -179,6 +181,7 @@ GEM parser (3.0.1.1) ast (~> 2.4.1) pg (1.2.3) + public_suffix (4.0.6) puma (5.3.2) nio4r (~> 2.0) racc (1.5.2) @@ -269,6 +272,9 @@ GEM tzinfo (2.0.4) concurrent-ruby (~> 1.0) unicode-display_width (1.6.1) + validate_url (1.0.13) + activemodel (>= 3.0.0) + public_suffix warden (1.2.9) rack (>= 2.0.9) warden-jwt_auth (0.5.0) @@ -292,6 +298,7 @@ DEPENDENCIES devise devise-jwt devise-two-factor + doorkeeper factory_bot jsonapi-serializer kaminari @@ -308,6 +315,7 @@ DEPENDENCIES rubocop (~> 0.75.1) rubocop-rspec sidekiq + validate_url RUBY VERSION ruby 3.0.1p64 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 85f5c2c1..2da5614e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -22,4 +22,23 @@ def presented_entity(resource_name, resource) def current_user_jwt_token request.env[Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY] end + + def user_signed_in? + super || current_resource_owner.present? + end + + def current_user + super || current_resource_owner + end + + def current_resource_owner + return @current_resource_owner if defined?(@current_resource_owner) + + unless valid_doorkeeper_token? + @current_resource_owner = nil + return + end + + @current_resource_owner = User.find(doorkeeper_token.resource_owner_id) + end end diff --git a/app/controllers/v1/repositories/commits_controller.rb b/app/controllers/v1/repositories/commits_controller.rb new file mode 100644 index 00000000..0d38adb6 --- /dev/null +++ b/app/controllers/v1/repositories/commits_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class V1::Repositories::CommitsController < ApplicationController + before_action :require_authentication + before_action :set_repository + before_action :set_branch, only: [:index] + before_action :set_commit, only: [:show] + + def index + render json: @branch.commits.order('committed_at DESC').map { |commit| presented_entity(:commit, commit) } + end + + def show + render json: presented_entity(:commit, @commit) + end + + private + + def set_repository + @repository = current_user.repositories.find(params[:repository_id]) + end + + def set_branch + @branch = @repository.branches.find_by!(name: params[:branch]) + end + + def set_commit + @commit = @repository.commits.find_by!(sha: params[:id]) + end +end diff --git a/app/controllers/v1/repositories/webhooks_controller.rb b/app/controllers/v1/repositories/webhooks_controller.rb new file mode 100644 index 00000000..8802e6f9 --- /dev/null +++ b/app/controllers/v1/repositories/webhooks_controller.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class V1::Repositories::WebhooksController < ApplicationController + before_action :require_authentication + before_action :set_repository + before_action :set_webhook, only: [:show, :update] + + def index + render json: @repository.webhooks.map { |webhook| presented_entity(:webhook, webhook) } + end + + def show + render json: presented_entity(:webhook, @webhook) + end + + def create + webhook = @repository.webhooks.build(webhook_params) + if webhook.save + render json: presented_entity(:webhook, webhook) + return + end + + render json: { errors: webhook.errors.full_messages }, status: :unprocessable_entity + end + + def update + if @webhook.update(webhook_params) + render json: presented_entity(:webhook, @webhook) + return + end + + render json: { errors: webhook.errors.full_messages }, status: :unprocessable_entity + end + + private + + def set_repository + @repository = current_user.repositories.find(params[:repository_id]) + end + + def set_webhook + @webhook = @repository.webhooks.find(params[:id]) + end + + def webhook_params + params.require(:webhook).permit(:name, :url, :active, :insecure_ssl) + end +end \ No newline at end of file diff --git a/app/controllers/v1/repositories_controller.rb b/app/controllers/v1/repositories_controller.rb index f57c6d1d..beb700ef 100644 --- a/app/controllers/v1/repositories_controller.rb +++ b/app/controllers/v1/repositories_controller.rb @@ -30,6 +30,6 @@ def sync private def set_repository - @repository = current_user.repositories.includes(:repository_permissions).find(params[:id]) + @repository = current_user.repositories.includes(:permissions).find(params[:id]) end end diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index cbdaec7d..ad4e25d0 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -27,7 +27,7 @@ def create set_provider_credentials(provider, errors) - unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission::OWNER) + unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission.permissions[:owner]) errors << 'Cannot set permission for user' raise ActiveRecord::Rollback end @@ -68,7 +68,7 @@ def authenticate ActiveRecord::Base.transaction do permission = current_user.server_provider_permissions.find_or_initialize_by(server_provider_id: @server_provider.id) unless permission.persisted? - permission.permission = ServerProviderPermission::MEMBER + permission.permission = ServerProviderPermission.permissions[:member] unless permission.save success = false raise ActiveRecord::Rollback @@ -101,10 +101,10 @@ def sync def repositories order = params[:sort_by] == 'last_synced_at' ? 'DESC' : 'ASC' repositories = @server_provider.repositories - .includes(:repository_permissions) + .includes(:permissions) .includes(:setting_objects) - .order(params[:sort_by] => order) - unless params[:filter].empty? + repositories = repositories.order(params[:sort_by] => order) if params[:sort_by].present? + if params[:filter].present? repositories = repositories.where('name LIKE ?', "%#{params[:filter]}%") end @@ -123,7 +123,7 @@ def add_by_url errors = [] provider = ServerProvider.find_by!(url: params[:url]) - unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission::MEMBER) + unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission.permissions[:member]) errors << 'Cannot set permission for user' raise ActiveRecord::Rollback end diff --git a/app/controllers/v1/webhooks_controller.rb b/app/controllers/v1/webhooks_controller.rb index a826db39..83b55fbd 100644 --- a/app/controllers/v1/webhooks_controller.rb +++ b/app/controllers/v1/webhooks_controller.rb @@ -17,6 +17,8 @@ def receive commit = ref.commits.find_by(sha: params[:sha]) || ref.commits.create(sha: params[:sha], repository: repository, user: user) head :unprocessable_entity and return unless commit + TriggerWebhooks.new(commit).call + head :ok end end diff --git a/app/jobs/webhook_trigger_job.rb b/app/jobs/webhook_trigger_job.rb new file mode 100644 index 00000000..ed065f3e --- /dev/null +++ b/app/jobs/webhook_trigger_job.rb @@ -0,0 +1,9 @@ +class WebhookTriggerJob < ApplicationJob + queue_as :default + + def perform(commit_id) + commit = Commit.find(commit_id) + + TriggerWebhooks.new(commit).call + end +end diff --git a/app/models/p4_server_provider.rb b/app/models/p4_server_provider.rb index f22797cb..02a47ac5 100644 --- a/app/models/p4_server_provider.rb +++ b/app/models/p4_server_provider.rb @@ -27,4 +27,12 @@ def commit_info_from_webhook(payload) bare_repo.commit_info(payload[:change_root], payload[:username]) end + + def provider_type + 'perforce' + end + + def default_branch + 'master' + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 21ca72c3..d5b459e4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -9,9 +9,10 @@ class Repository < ApplicationRecord validates_presence_of :name, :url, :server_provider_id has_many :refs, dependent: :destroy - has_many :repository_permissions - has_many :users, through: :repository_permissions has_many :permissions, class_name: 'RepositoryPermission', dependent: :delete_all + has_many :users, through: :permissions + has_many :commits, dependent: :destroy + has_many :webhooks, dependent: :destroy def branches refs.branch diff --git a/app/models/server_provider.rb b/app/models/server_provider.rb index 7d7a5080..932fb78b 100644 --- a/app/models/server_provider.rb +++ b/app/models/server_provider.rb @@ -26,6 +26,14 @@ def commit_info_from_webhook(payload) raise NotImplementedError end + def provider_type + raise NotImplementedError + end + + def default_branch + raise NotImplementedError + end + private def generate_listener_token diff --git a/app/models/server_provider_permission.rb b/app/models/server_provider_permission.rb index 37498c6f..d7ac7eb9 100644 --- a/app/models/server_provider_permission.rb +++ b/app/models/server_provider_permission.rb @@ -6,10 +6,5 @@ class ServerProviderPermission < ApplicationRecord has_one :setting, class_name: 'ServerProviderUserSetting', foreign_key: :server_provider_user_id, dependent: :delete - OWNER = 1 - MEMBER = 2 - - def owner? - permission == OWNER - end + enum permission: [:owner, :member] end diff --git a/app/models/user.rb b/app/models/user.rb index 44792b65..97a4a2fd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,6 +13,16 @@ class User < ApplicationRecord jwt_revocation_strategy: self, otp_secret_encryption_key: Settings.otp_secret_encryption_key + has_many :access_grants, + class_name: 'Doorkeeper::AccessGrant', + foreign_key: :resource_owner_id, + dependent: :delete_all + + has_many :access_tokens, + class_name: 'Doorkeeper::AccessToken', + foreign_key: :resource_owner_id, + dependent: :delete_all + validate :password_complexity has_many :server_provider_permissions diff --git a/app/models/webhook.rb b/app/models/webhook.rb new file mode 100644 index 00000000..8461e0f4 --- /dev/null +++ b/app/models/webhook.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Webhook < ApplicationRecord + belongs_to :repository + + validates_presence_of :repository_id, :name, :url + + scope :active, -> { where(active: true) } + validates :url, url: { schemes: %w(https http) } +end diff --git a/app/serializers/commit_serializer.rb b/app/serializers/commit_serializer.rb new file mode 100644 index 00000000..0a825802 --- /dev/null +++ b/app/serializers/commit_serializer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CommitSerializer < ApplicationSerializer + attributes :id, :message, :sha, :committed_at + + attributes(:author) do |commit| + { + name: commit.user.name || commit.user.email, + email: commit.user.email, + } + end +end diff --git a/app/serializers/ref_serializer.rb b/app/serializers/ref_serializer.rb index 32091828..db59e3c5 100644 --- a/app/serializers/ref_serializer.rb +++ b/app/serializers/ref_serializer.rb @@ -2,4 +2,10 @@ class RefSerializer < ApplicationSerializer attributes :id, :name + + attributes(:commit) do |ref| + commit = ref.commits.order('committed_at DESC').first + + { sha: commit.sha } if commit.present? + end end diff --git a/app/serializers/repository_serializer.rb b/app/serializers/repository_serializer.rb index 29673300..556e0189 100644 --- a/app/serializers/repository_serializer.rb +++ b/app/serializers/repository_serializer.rb @@ -3,7 +3,14 @@ class RepositorySerializer < ApplicationSerializer attributes :id, :name, :url, :token, :last_synced_at, :server_provider_id - attributes(:permission) do |repo| - repo.repository_permissions&.first&.permission + attributes(:permission) { |repo| repo.permissions&.first&.permission } + attributes(:default_branch) { |repo| repo.server_provider.default_branch } + attributes(:url) { |repo| URI.join(Settings.web_url, "servers/#{repo.server_provider_id}") } + attributes(:owner) do |repo| + { + id: repo.server_provider.id, + } end + attributes(:slug) { |repo| "#{repo.server_provider.name}/#{repo.name}" } + attributes(:server_type) { |repo| repo.server_provider.provider_type } end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 3494a620..a5d1813e 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -5,4 +5,5 @@ class UserSerializer < ApplicationSerializer attribute(:login) { |user| user.email } attribute(:emails) { |user| [ user.email ] } attribute(:servers) { |user| user.server_providers.map(&:id) } + attribute(:uuid) { |user| user.id } end diff --git a/app/serializers/webhook_serializer.rb b/app/serializers/webhook_serializer.rb new file mode 100644 index 00000000..ed85a36f --- /dev/null +++ b/app/serializers/webhook_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class WebhookSerializer < ApplicationSerializer + attributes :id, :name, :url, :active, :insecure_ssl, :created_at +end diff --git a/app/services/trigger_webhooks.rb b/app/services/trigger_webhooks.rb new file mode 100644 index 00000000..6f3701dd --- /dev/null +++ b/app/services/trigger_webhooks.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class TriggerWebhooks + class WebhookError < StandardError; end + + def initialize(commit) + @commit = commit + @ref = commit.ref + @repository = @ref.repository + @server_provider = @repository.server_provider + end + + def call + @repository.webhooks.active.each do |webhook| + begin + process_webhook(webhook) + rescue => e + Rails.logger.error "An error happened while processing webhook id=#{webhook.id} name=#{webhook.name}: #{e.message}" + Rails.logger.error "Partial backtrace:" + Rails.logger.error(e.backtrace.first(20).join("\n")) + end + end + end + + private + + def process_webhook(webhook) + Rails.logger.info "Triggering webhook id=#{webhook.id} name=#{webhook.name}" + uri = URI(webhook.url) + + res = Net::HTTP.post( + uri, + webhook_payload(webhook), + 'Content-Type' => 'application/json' + ) + + raise WebhookError.new("Request failed: code=#{res.code}, body=#{res.body}") unless res.is_a?(Net::HTTPSuccess) + end + + def webhook_payload(webhook) + JSON.dump( + commit: { + sha: @commit.sha, + message: @commit.message, + committed_at: @commit.committed_at, + } + ) + end +end \ No newline at end of file diff --git a/config/environments/development.rb b/config/environments/development.rb index 16255fcd..9937cc62 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -70,4 +70,6 @@ address: 'mailhog', port: 1025, } + + config.hosts.clear end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 00000000..0056d63c --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,490 @@ +# frozen_string_literal: true + +Doorkeeper.configure do + # Change the ORM that doorkeeper will use (requires ORM extensions installed). + # Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms + orm :active_record + + # This block will be called to check whether the resource owner is authenticated or not. + resource_owner_authenticator do + # Put your resource owner authentication logic here. + # Example implementation: + current_user || warden.authenticate!(scope: :user) # redirect_to(URI.join(Settings.web_url, 'sign_in').to_s) + end + + # If you didn't skip applications controller from Doorkeeper routes in your application routes.rb + # file then you need to declare this block in order to restrict access to the web interface for + # adding oauth authorized applications. In other case it will return 403 Forbidden response + # every time somebody will try to access the admin web interface. + # + # admin_authenticator do + # # Put your admin authentication logic here. + # # Example implementation: + # + # if current_user + # head :forbidden unless current_user.admin? + # else + # redirect_to sign_in_url + # end + # end + + # You can use your own model classes if you need to extend (or even override) default + # Doorkeeper models such as `Application`, `AccessToken` and `AccessGrant. + # + # Be default Doorkeeper ActiveRecord ORM uses it's own classes: + # + # access_token_class "Doorkeeper::AccessToken" + # access_grant_class "Doorkeeper::AccessGrant" + # application_class "Doorkeeper::Application" + # + # Don't forget to include Doorkeeper ORM mixins into your custom models: + # + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken - for access token + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant - for access grant + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::Application - for application (OAuth2 clients) + # + # For example: + # + # access_token_class "MyAccessToken" + # + # class MyAccessToken < ApplicationRecord + # include ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken + # + # self.table_name = "hey_i_wanna_my_name" + # + # def destroy_me! + # destroy + # end + # end + + # Enables polymorphic Resource Owner association for Access Tokens and Access Grants. + # By default this option is disabled. + # + # Make sure you properly setup you database and have all the required columns (run + # `bundle exec rails generate doorkeeper:enable_polymorphic_resource_owner` and execute Rails + # migrations). + # + # If this option enabled, Doorkeeper will store not only Resource Owner primary key + # value, but also it's type (class name). See "Polymorphic Associations" section of + # Rails guides: https://guides.rubyonrails.org/association_basics.html#polymorphic-associations + # + # [NOTE] If you apply this option on already existing project don't forget to manually + # update `resource_owner_type` column in the database and fix migration template as it will + # set NOT NULL constraint for Access Grants table. + # + # use_polymorphic_resource_owner + + # If you are planning to use Doorkeeper in Rails 5 API-only application, then you might + # want to use API mode that will skip all the views management and change the way how + # Doorkeeper responds to a requests. + # + api_only + + # Enforce token request content type to application/x-www-form-urlencoded. + # It is not enabled by default to not break prior versions of the gem. + # + enforce_content_type + + # Authorization Code expiration time (default: 10 minutes). + # + # authorization_code_expires_in 10.minutes + + # Access token expiration time (default: 2 hours). + # If you want to disable expiration, set this to `nil`. + # + access_token_expires_in 2.hours + + # Assign custom TTL for access tokens. Will be used instead of access_token_expires_in + # option if defined. In case the block returns `nil` value Doorkeeper fallbacks to + # +access_token_expires_in+ configuration option value. If you really need to issue a + # non-expiring access token (which is not recommended) then you need to return + # Float::INFINITY from this block. + # + # `context` has the following properties available: + # + # * `client` - the OAuth client application (see Doorkeeper::OAuth::Client) + # * `grant_type` - the grant type of the request (see Doorkeeper::OAuth) + # * `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) + # * `resource_owner` - authorized resource owner instance (if present) + # + # custom_access_token_expires_in do |context| + # context.client.additional_settings.implicit_oauth_expiration + # end + + # Use a custom class for generating the access token. + # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator + # + # access_token_generator '::Doorkeeper::JWT' + + # The controller +Doorkeeper::ApplicationController+ inherits from. + # Defaults to +ActionController::Base+ unless +api_only+ is set, which changes the default to + # +ActionController::API+. The return value of this option must be a stringified class name. + # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-controllers + # + # base_controller 'ApplicationController' + + # Reuse access token for the same resource owner within an application (disabled by default). + # + # This option protects your application from creating new tokens before old valid one becomes + # expired so your database doesn't bloat. Keep in mind that when this option is `on` Doorkeeper + # doesn't updates existing token expiration time, it will create a new token instead. + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 + # + # You can not enable this option together with +hash_token_secrets+. + # + # reuse_access_token + + # In case you enabled `reuse_access_token` option Doorkeeper will try to find matching + # token using `matching_token_for` Access Token API that searches for valid records + # in batches in order not to pollute the memory with all the database records. By default + # Doorkeeper uses batch size of 10 000 records. You can increase or decrease this value + # depending on your needs and server capabilities. + # + # token_lookup_batch_size 10_000 + + # Set a limit for token_reuse if using reuse_access_token option + # + # This option limits token_reusability to some extent. + # If not set then access_token will be reused unless it expires. + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1189 + # + # This option should be a percentage(i.e. (0,100]) + # + # token_reuse_limit 100 + + # Only allow one valid access token obtained via client credentials + # per client. If a new access token is obtained before the old one + # expired, the old one gets revoked (disabled by default) + # + # When enabling this option, make sure that you do not expect multiple processes + # using the same credentials at the same time (e.g. web servers spanning + # multiple machines and/or processes). + # + # revoke_previous_client_credentials_token + + # Hash access and refresh tokens before persisting them. + # This will disable the possibility to use +reuse_access_token+ + # since plain values can no longer be retrieved. + # + # Note: If you are already a user of doorkeeper and have existing tokens + # in your installation, they will be invalid without adding 'fallback: :plain'. + # + # hash_token_secrets + # By default, token secrets will be hashed using the + # +Doorkeeper::Hashing::SHA256+ strategy. + # + # If you wish to use another hashing implementation, you can override + # this strategy as follows: + # + # hash_token_secrets using: '::Doorkeeper::Hashing::MyCustomHashImpl' + # + # Keep in mind that changing the hashing function will invalidate all existing + # secrets, if there are any. + + # Hash application secrets before persisting them. + # + # hash_application_secrets + # + # By default, applications will be hashed + # with the +Doorkeeper::SecretStoring::SHA256+ strategy. + # + # If you wish to use bcrypt for application secret hashing, uncomment + # this line instead: + # + # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt' + + # When the above option is enabled, and a hashed token or secret is not found, + # you can allow to fall back to another strategy. For users upgrading + # doorkeeper and wishing to enable hashing, you will probably want to enable + # the fallback to plain tokens. + # + # This will ensure that old access tokens and secrets + # will remain valid even if the hashing above is enabled. + # + # This can be done by adding 'fallback: plain', e.g. : + # + # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt', fallback: :plain + + # Issue access tokens with refresh token (disabled by default), you may also + # pass a block which accepts `context` to customize when to give a refresh + # token or not. Similar to +custom_access_token_expires_in+, `context` has + # the following properties: + # + # `client` - the OAuth client application (see Doorkeeper::OAuth::Client) + # `grant_type` - the grant type of the request (see Doorkeeper::OAuth) + # `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) + # + use_refresh_token + + # Provide support for an owner to be assigned to each registered application (disabled by default) + # Optional parameter confirmation: true (default: false) if you want to enforce ownership of + # a registered application + # NOTE: you must also run the rails g doorkeeper:application_owner generator + # to provide the necessary support + # + # enable_application_owner confirmation: false + + # Define access token scopes for your provider + # For more information go to + # https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes + # + # default_scopes :public + # optional_scopes :write, :update + + # Allows to restrict only certain scopes for grant_type. + # By default, all the scopes will be available for all the grant types. + # + # Keys to this hash should be the name of grant_type and + # values should be the array of scopes for that grant type. + # Note: scopes should be from configured_scopes (i.e. default or optional) + # + # scopes_by_grant_type password: [:write], client_credentials: [:update] + + # Forbids creating/updating applications with arbitrary scopes that are + # not in configuration, i.e. +default_scopes+ or +optional_scopes+. + # (disabled by default) + # + # enforce_configured_scopes + + # Change the way client credentials are retrieved from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:client_id` and `:client_secret` params from the `params` object. + # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + # + # client_credentials :from_basic, :from_params + + # Change the way access token is authenticated from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:access_token` or `:bearer_token` params from the `params` object. + # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + # + # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param + + # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled + # by default in non-development environments). OAuth2 delegates security in + # communication to the HTTPS protocol so it is wise to keep this enabled. + # + # Callable objects such as proc, lambda, block or any object that responds to + # #call can be used in order to allow conditional checks (to allow non-SSL + # redirects to localhost for example). + # + # force_ssl_in_redirect_uri !Rails.env.development? + # + # force_ssl_in_redirect_uri { |uri| uri.host != 'localhost' } + + # Specify what redirect URI's you want to block during Application creation. + # Any redirect URI is whitelisted by default. + # + # You can use this option in order to forbid URI's with 'javascript' scheme + # for example. + # + # forbid_redirect_uri { |uri| uri.scheme.to_s.downcase == 'javascript' } + + # Allows to set blank redirect URIs for Applications in case Doorkeeper configured + # to use URI-less OAuth grant flows like Client Credentials or Resource Owner + # Password Credentials. The option is on by default and checks configured grant + # types, but you **need** to manually drop `NOT NULL` constraint from `redirect_uri` + # column for `oauth_applications` database table. + # + # You can completely disable this feature with: + # + # allow_blank_redirect_uri false + # + # Or you can define your custom check: + # + # allow_blank_redirect_uri do |grant_flows, client| + # client.superapp? + # end + + # Specify how authorization errors should be handled. + # By default, doorkeeper renders json errors when access token + # is invalid, expired, revoked or has invalid scopes. + # + # If you want to render error response yourself (i.e. rescue exceptions), + # set +handle_auth_errors+ to `:raise` and rescue Doorkeeper::Errors::InvalidToken + # or following specific errors: + # + # Doorkeeper::Errors::TokenForbidden, Doorkeeper::Errors::TokenExpired, + # Doorkeeper::Errors::TokenRevoked, Doorkeeper::Errors::TokenUnknown + # + # handle_auth_errors :raise + + # Customize token introspection response. + # Allows to add your own fields to default one that are required by the OAuth spec + # for the introspection response. It could be `sub`, `aud` and so on. + # This configuration option can be a proc, lambda or any Ruby object responds + # to `.call` method and result of it's invocation must be a Hash. + # + # custom_introspection_response do |token, context| + # { + # "sub": "Z5O3upPC88QrAjx00dis", + # "aud": "https://protected.example.net/resource", + # "username": User.find(token.resource_owner_id).username + # } + # end + # + # or + # + # custom_introspection_response CustomIntrospectionResponder + + # Specify what grant flows are enabled in array of Strings. The valid + # strings and the flows they enable are: + # + # "authorization_code" => Authorization Code Grant Flow + # "implicit" => Implicit Grant Flow + # "password" => Resource Owner Password Credentials Grant Flow + # "client_credentials" => Client Credentials Grant Flow + # + # If not specified, Doorkeeper enables authorization_code and + # client_credentials. + # + # implicit and password grant flows have risks that you should understand + # before enabling: + # http://tools.ietf.org/html/rfc6819#section-4.4.2 + # http://tools.ietf.org/html/rfc6819#section-4.4.3 + # + # grant_flows %w[authorization_code client_credentials] + + # Allows to customize OAuth grant flows that +each+ application support. + # You can configure a custom block (or use a class respond to `#call`) that must + # return `true` in case Application instance supports requested OAuth grant flow + # during the authorization request to the server. This configuration +doesn't+ + # set flows per application, it only allows to check if application supports + # specific grant flow. + # + # For example you can add an additional database column to `oauth_applications` table, + # say `t.array :grant_flows, default: []`, and store allowed grant flows that can + # be used with this application there. Then when authorization requested Doorkeeper + # will call this block to check if specific Application (passed with client_id and/or + # client_secret) is allowed to perform the request for the specific grant type + # (authorization, password, client_credentials, etc). + # + # Example of the block: + # + # ->(flow, client) { client.grant_flows.include?(flow) } + # + # In case this option invocation result is `false`, Doorkeeper server returns + # :unauthorized_client error and stops the request. + # + # @param allow_grant_flow_for_client [Proc] Block or any object respond to #call + # @return [Boolean] `true` if allow or `false` if forbid the request + # + # allow_grant_flow_for_client do |grant_flow, client| + # # `grant_flows` is an Array column with grant + # # flows that application supports + # + # client.grant_flows.include?(grant_flow) + # end + + # If you need arbitrary Resource Owner-Client authorization you can enable this option + # and implement the check your need. Config option must respond to #call and return + # true in case resource owner authorized for the specific application or false in other + # cases. + # + # Be default all Resource Owners are authorized to any Client (application). + # + # authorize_resource_owner_for_client do |client, resource_owner| + # resource_owner.admin? || client.owners_whitelist.include?(resource_owner) + # end + + # Hook into the strategies' request & response life-cycle in case your + # application needs advanced customization or logging: + # + # before_successful_strategy_response do |request| + # puts "BEFORE HOOK FIRED! #{request}" + # end + # + # after_successful_strategy_response do |request, response| + # puts "AFTER HOOK FIRED! #{request}, #{response}" + # end + + # Hook into Authorization flow in order to implement Single Sign Out + # or add any other functionality. Inside the block you have an access + # to `controller` (authorizations controller instance) and `context` + # (Doorkeeper::OAuth::Hooks::Context instance) which provides pre auth + # or auth objects with issued token based on hook type (before or after). + # + # before_successful_authorization do |controller, context| + # Rails.logger.info(controller.request.params.inspect) + # + # Rails.logger.info(context.pre_auth.inspect) + # end + # + # after_successful_authorization do |controller, context| + # controller.session[:logout_urls] << + # Doorkeeper::Application + # .find_by(controller.request.params.slice(:redirect_uri)) + # .logout_uri + # + # Rails.logger.info(context.auth.inspect) + # Rails.logger.info(context.issued_token) + # end + + # Under some circumstances you might want to have applications auto-approved, + # so that the user skips the authorization step. + # For example if dealing with a trusted application. + # + # skip_authorization do |resource_owner, client| + # client.superapp? or resource_owner.admin? + # end + + # Configure custom constraints for the Token Introspection request. + # By default this configuration option allows to introspect a token by another + # token of the same application, OR to introspect the token that belongs to + # authorized client (from authenticated client) OR when token doesn't + # belong to any client (public token). Otherwise requester has no access to the + # introspection and it will return response as stated in the RFC. + # + # Block arguments: + # + # @param token [Doorkeeper::AccessToken] + # token to be introspected + # + # @param authorized_client [Doorkeeper::Application] + # authorized client (if request is authorized using Basic auth with + # Client Credentials for example) + # + # @param authorized_token [Doorkeeper::AccessToken] + # Bearer token used to authorize the request + # + # In case the block returns `nil` or `false` introspection responses with 401 status code + # when using authorized token to introspect, or you'll get 200 with { "active": false } body + # when using authorized client to introspect as stated in the + # RFC 7662 section 2.2. Introspection Response. + # + # Using with caution: + # Keep in mind that these three parameters pass to block can be nil as following case: + # `authorized_client` is nil if and only if `authorized_token` is present, and vice versa. + # `token` will be nil if and only if `authorized_token` is present. + # So remember to use `&` or check if it is present before calling method on + # them to make sure you doesn't get NoMethodError exception. + # + # You can define your custom check: + # + # allow_token_introspection do |token, authorized_client, authorized_token| + # if authorized_token + # # customize: require `introspection` scope + # authorized_token.application == token&.application || + # authorized_token.scopes.include?("introspection") + # elsif token.application + # # `protected_resource` is a new database boolean column, for example + # authorized_client == token.application || authorized_client.protected_resource? + # else + # # public token (when token.application is nil, token doesn't belong to any application) + # true + # end + # end + # + # Or you can completely disable any token introspection: + # + # allow_token_introspection false + # + # If you need to block the request at all, then configure your routes.rb or web-server + # like nginx to forbid the request. + + # WWW-Authenticate Realm (default: "Doorkeeper"). + # + # realm "Doorkeeper" +end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml new file mode 100644 index 00000000..e3cf04ca --- /dev/null +++ b/config/locales/doorkeeper.en.yml @@ -0,0 +1,148 @@ +en: + activerecord: + attributes: + doorkeeper/application: + name: 'Name' + redirect_uri: 'Redirect URI' + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + unspecified_scheme: 'must specify a scheme.' + relative_uri: 'must be an absolute URI.' + secured_uri: 'must be an HTTPS/SSL URI.' + forbidden_uri: 'is forbidden by the server.' + scopes: + not_match_configured: "doesn't match configured on the server." + + doorkeeper: + applications: + confirmations: + destroy: 'Are you sure?' + buttons: + edit: 'Edit' + destroy: 'Destroy' + submit: 'Submit' + cancel: 'Cancel' + authorize: 'Authorize' + form: + error: 'Whoops! Check your form for possible errors' + help: + confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.' + redirect_uri: 'Use one line per URI' + blank_redirect_uri: "Leave it blank if you configured your provider to use Client Credentials, Resource Owner Password Credentials or any other grant type that doesn't require redirect URI." + scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.' + edit: + title: 'Edit application' + index: + title: 'Your applications' + new: 'New Application' + name: 'Name' + callback_url: 'Callback URL' + confidential: 'Confidential?' + actions: 'Actions' + confidentiality: + 'yes': 'Yes' + 'no': 'No' + new: + title: 'New Application' + show: + title: 'Application: %{name}' + application_id: 'UID' + secret: 'Secret' + secret_hashed: 'Secret hashed' + scopes: 'Scopes' + confidential: 'Confidential' + callback_urls: 'Callback urls' + actions: 'Actions' + not_defined: 'Not defined' + + authorizations: + buttons: + authorize: 'Authorize' + deny: 'Deny' + error: + title: 'An error has occurred' + new: + title: 'Authorization required' + prompt: 'Authorize %{client_name} to use your account?' + able_to: 'This application will be able to' + show: + title: 'Authorization code' + form_post: + title: 'Submit this form' + + authorized_applications: + confirmations: + revoke: 'Are you sure?' + buttons: + revoke: 'Revoke' + index: + title: 'Your authorized applications' + application: 'Application' + created_at: 'Created At' + date_format: '%Y-%m-%d %H:%M:%S' + + pre_authorization: + status: 'Pre-authorization' + + errors: + messages: + # Common error messages + invalid_request: + unknown: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' + missing_param: 'Missing required parameter: %{value}.' + request_not_authorized: 'Request need to be authorized. Required parameter for authorizing request is missing or invalid.' + invalid_redirect_uri: "The requested redirect uri is malformed or doesn't match client redirect URI." + unauthorized_client: 'The client is not authorized to perform this request using this method.' + access_denied: 'The resource owner or authorization server denied the request.' + invalid_scope: 'The requested scope is invalid, unknown, or malformed.' + invalid_code_challenge_method: 'The code challenge method must be plain or S256.' + server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.' + temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.' + + # Configuration error messages + credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.' + resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfigured.' + admin_authenticator_not_configured: 'Access to admin panel is forbidden due to Doorkeeper.configure.admin_authenticator being unconfigured.' + + # Access grant errors + unsupported_response_type: 'The authorization server does not support this response type.' + unsupported_response_mode: 'The authorization server does not support this response mode.' + + # Access token errors + invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.' + invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' + unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.' + + invalid_token: + revoked: "The access token was revoked" + expired: "The access token expired" + unknown: "The access token is invalid" + revoke: + unauthorized: "You are not authorized to revoke this token" + + flash: + applications: + create: + notice: 'Application created.' + destroy: + notice: 'Application deleted.' + update: + notice: 'Application updated.' + authorized_applications: + destroy: + notice: 'Application revoked.' + + layouts: + admin: + title: 'Doorkeeper' + nav: + oauth2_provider: 'OAuth2 Provider' + applications: 'Applications' + home: 'Home' + application: + title: 'OAuth authorization required' diff --git a/config/routes.rb b/config/routes.rb index 7a9187af..87cfe070 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,12 @@ post :listener, to: 'v1/webhooks#receive' + scope :v1 do + use_doorkeeper do + skip_controllers :applications, :token_info + end + end + scope :v1, module: :v1 do devise_for :users, controllers: { @@ -61,6 +67,8 @@ resources :repositories, only: [:show] do resources :branches, controller: 'repositories/branches', only: [:index, :show] + resources :commits, controller: 'repositories/commits', only: [:index, :show] + resources :webhooks, controller: 'repositories/webhooks', only: [:index, :show, :create, :update] resources :token, controller: 'repositories/token', only: [] do collection do patch :update diff --git a/lib/travis/vcs_proxy/repositories/p4.rb b/lib/travis/vcs_proxy/repositories/p4.rb index 8d25cd77..6c679da7 100644 --- a/lib/travis/vcs_proxy/repositories/p4.rb +++ b/lib/travis/vcs_proxy/repositories/p4.rb @@ -53,6 +53,24 @@ def users [] end + def commits(branch_name) + user_map = users.each_with_object({}) do |user, memo| + memo[user[:name]] = user[:email] + end + p4.run_changes('-l', "//#{@repository.name}/#{branch_name}/...").map do |change| + puts change.inspect + next unless email = user_map[change['user']] + next unless user = User.find_by(email: email) + + { + sha: change['change'], + user: user, + message: change['desc'], + committed_at: Time.at(change['time'].to_i) + } + end + end + def permissions @permissions ||= users.each_with_object({}) do |user, memo| memo[user[:email]] = p4.run_protects('-u', user[:name], '-M', "//#{@repository.name}/...").first['permMax'] diff --git a/lib/travis/vcs_proxy/sync/repository.rb b/lib/travis/vcs_proxy/sync/repository.rb index 4132adec..7a263719 100644 --- a/lib/travis/vcs_proxy/sync/repository.rb +++ b/lib/travis/vcs_proxy/sync/repository.rb @@ -22,32 +22,40 @@ def sync return if username.blank? || token.blank? @repository.repo.branches.each do |branch| - @repository.branches.find_or_create_by!(name: branch[:name].sub(/\A\/\/#{Regexp.escape(@repository.name)}\//, '')) + branch_name = branch[:name].sub(/\A\/\/#{Regexp.escape(@repository.name)}\//, '') + branch = @repository.branches.find_or_create_by!(name: branch_name) + + @repository.repo.commits(branch_name).each do |commit| + next if branch.commits.where(sha: commit[:sha]).exists? + + branch.commits.create!(commit.merge(repository_id: @repository.id)) + end end perms = @repository.repo.permissions - return if perms.blank? - repo_emails = perms.keys - users = ::User.where(email: repo_emails).group_by(&:email) - db_emails = @repository.users.pluck(:email) - - # Remove users that don't have access anymore - (db_emails - repo_emails).each do |email| - users[email].first.repository_permission(@repository.id).delete - end + if perms.present? + repo_emails = perms.keys + users = ::User.where(email: repo_emails).group_by(&:email) + db_emails = @repository.users.pluck(:email) - perms.each do |email, permission| - next unless users.has_key?(email) - user = users[email].first - perm = user.repository_permission(@repository.id) - if permission == 'none' - perm&.delete - return + # Remove users that don't have access anymore + (db_emails - repo_emails).each do |email| + users[email].first.repository_permission(@repository.id).delete end - perm ||= user.repository_permissions.build(repository_id: @repository.id) - perm.permission = permission - perm.save! + perms.each do |email, permission| + next unless users.has_key?(email) + user = users[email].first + perm = user.repository_permission(@repository.id) + if permission == 'none' + perm&.delete + return + end + + perm ||= user.repository_permissions.build(repository_id: @repository.id) + perm.permission = permission + perm.save! + end end @repository.last_synced_at = Time.now diff --git a/spec/models/webhook_spec.rb b/spec/models/webhook_spec.rb new file mode 100644 index 00000000..e7e79de3 --- /dev/null +++ b/spec/models/webhook_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Webhook, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From f9c3d06d2eb2825e57a0c09dd82591b1887d5701 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Thu, 9 Sep 2021 11:54:43 +0300 Subject: [PATCH 41/65] Initial unit tests and travis --- .rspec | 1 + .rubocop.yml | 67 +++++++++++++ .travis.yml | 26 +++++ Gemfile | 1 + Gemfile.lock | 7 ++ Rakefile | 12 +++ db/schema.rb | 96 +++++++++++++++---- .../repositories/branches_controller_spec.rb | 5 + .../repositories/commits_controller_spec.rb | 5 + .../v1/repositories/token_controller_spec.rb | 5 + .../repositories/webhooks_controller_spec.rb | 5 + .../v1/repositories_controller_spec.rb | 5 + .../v1/server_providers_controller_spec.rb | 5 + .../v1/users/confirmations_controller_spec.rb | 5 + .../v1/users/registrations_controller_spec.rb | 5 + .../v1/users/sessions_controller_spec.rb | 5 + .../users/two_factor_auth_controller_spec.rb | 5 + spec/controllers/v1/users_controller_spec.rb | 5 + .../v1/webhooks_controller_spec.rb | 5 + spec/factories.rb | 34 +++++++ spec/jobs/sync_job_spec.rb | 5 - spec/models/jwt_deny_list_spec.rb | 5 - spec/models/p4_service_provider_spec.rb | 5 + spec/models/repository_spec.rb | 5 + spec/models/user_spec.rb | 47 ++++++++- spec/rails_helper.rb | 88 +++++++++++++++++ spec/spec_helper.rb | 96 +++++++++++++++++++ 27 files changed, 528 insertions(+), 27 deletions(-) create mode 100644 .rspec create mode 100644 .rubocop.yml create mode 100644 .travis.yml create mode 100644 spec/controllers/v1/repositories/branches_controller_spec.rb create mode 100644 spec/controllers/v1/repositories/commits_controller_spec.rb create mode 100644 spec/controllers/v1/repositories/token_controller_spec.rb create mode 100644 spec/controllers/v1/repositories/webhooks_controller_spec.rb create mode 100644 spec/controllers/v1/repositories_controller_spec.rb create mode 100644 spec/controllers/v1/server_providers_controller_spec.rb create mode 100644 spec/controllers/v1/users/confirmations_controller_spec.rb create mode 100644 spec/controllers/v1/users/registrations_controller_spec.rb create mode 100644 spec/controllers/v1/users/sessions_controller_spec.rb create mode 100644 spec/controllers/v1/users/two_factor_auth_controller_spec.rb create mode 100644 spec/controllers/v1/users_controller_spec.rb create mode 100644 spec/controllers/v1/webhooks_controller_spec.rb create mode 100644 spec/factories.rb delete mode 100644 spec/jobs/sync_job_spec.rb delete mode 100644 spec/models/jwt_deny_list_spec.rb create mode 100644 spec/models/p4_service_provider_spec.rb create mode 100644 spec/models/repository_spec.rb create mode 100644 spec/rails_helper.rb create mode 100644 spec/spec_helper.rb diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..0a082bd3 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,67 @@ +require: + - rubocop-rspec + +AllCops: + Exclude: + - vendor/**/* + - tmp/**/* + - lib/tasks/* + - bin/* + - rgloader/* + TargetRubyVersion: 2.6 + +Style/Documentation: + Enabled: false + +Metrics/LineLength: + Exclude: + - config/initializers/* + Enabled: true + Max: 120 + +Metrics/MethodLength: + CountComments: false + Max: 15 + Exclude: + - spec/**/*.rb + +Metrics/BlockLength: + Exclude: + - spec/**/*.rb + - config/routes.rb + +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: comma + +RSpec/DescribedClass: + EnforcedStyle: explicit + +RSpec/MultipleExpectations: + Enabled: false + +RSpec/NestedGroups: + Enabled: false + +RSpec/VerifiedDoubles: + Enabled: false + +RSpec/ExampleLength: + Enabled: false + +RSpec/AnyInstance: + Enabled: false + +RSpec/MessageSpies: + EnforcedStyle: receive + +RSpec/NamedSubject: + Enabled: false + +RSpec/SubjectStub: + Enabled: false + +Lint/UnusedMethodArgument: + Enabled: false diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..d9088f91 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +language: ruby +dist: xenial + +addons: + postgresql: 12.1 + +rvm: 3.0.1 + +env: + global: + - PATH=/snap/bin:$PATH + +cache: bundler + +jobs: + include: + - stage: "rubocop" + scripe: bundle exec rubocop + - stage: "rspec" + script: bundle exec rspec + before_install: + - "gem install bundler -v 2.1.4" + before_script: + - "RAILS_ENV=test bundle exec rake db:schema:load" + services: + - redis-server diff --git a/Gemfile b/Gemfile index 26bd4342..7b42cfa0 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,7 @@ end group :test do gem 'rspec' + gem 'database_cleaner' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 90ffe2ae..1fd06a0c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,6 +75,12 @@ GEM dry-validation (~> 1.0, >= 1.0.0) connection_pool (2.2.5) crass (1.0.6) + database_cleaner (2.0.1) + database_cleaner-active_record (~> 2.0.0) + database_cleaner-active_record (2.0.1) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0.0) + database_cleaner-core (2.0.1) deep_merge (1.2.1) devise (4.8.0) bcrypt (~> 3.0) @@ -295,6 +301,7 @@ DEPENDENCIES brakeman byebug config + database_cleaner devise devise-jwt devise-two-factor diff --git a/Rakefile b/Rakefile index 9a5ea738..a9a8a3a8 100644 --- a/Rakefile +++ b/Rakefile @@ -4,3 +4,15 @@ require_relative "config/application" Rails.application.load_tasks + +unless ENV['RACK_ENV'] == 'production' + require 'rspec/core/rake_task' + RSpec::Core::RakeTask.new(:spec) + + require 'rubocop/rake_task' + RuboCop::RakeTask.new do |t| + t.patterns = ['{app,spec}/**/*.rb', '{Rake,Gem}file', 'config.ru'] + end + + task default: %i[spec rubocop] +end diff --git a/db/schema.rb b/db/schema.rb index 9cfa7f7e..650a96c1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_06_17_063016) do +ActiveRecord::Schema.define(version: 2021_08_18_115729) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -20,31 +20,54 @@ t.integer "user_id", null: false t.integer "repository_id", null: false t.integer "ref_id", null: false + t.string "message" + t.datetime "committed_at", null: false t.datetime "created_at", null: false + t.index ["ref_id", "sha"], name: "index_commits_on_ref_id_and_sha" + t.index ["repository_id"], name: "index_commits_on_repository_id" + t.index ["user_id"], name: "index_commits_on_user_id" end - create_table "jwt_deny_lists", force: :cascade do |t| - t.string "jti", null: false - t.datetime "exp", null: false - t.index ["jti"], name: "index_jwt_deny_lists_on_jti" + create_table "oauth_access_grants", force: :cascade do |t| + t.bigint "resource_owner_id", null: false + t.bigint "application_id", null: false + t.string "token", null: false + t.integer "expires_in", null: false + t.text "redirect_uri", null: false + t.datetime "created_at", null: false + t.datetime "revoked_at" + t.string "scopes", default: "", null: false + t.index ["application_id"], name: "index_oauth_access_grants_on_application_id" + t.index ["resource_owner_id"], name: "index_oauth_access_grants_on_resource_owner_id" + t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true end create_table "oauth_access_tokens", force: :cascade do |t| - t.integer "resource_owner_id", null: false - t.integer "application_id", null: false - t.string "token" + t.bigint "resource_owner_id" + t.bigint "application_id", null: false + t.string "token", null: false t.string "refresh_token" t.integer "expires_in" - t.datetime "created_at" t.datetime "revoked_at" + t.datetime "created_at", null: false + t.string "scopes" + t.string "previous_refresh_token", default: "", null: false + t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id" + t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true + t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" + t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true end create_table "oauth_applications", force: :cascade do |t| t.string "name", null: false - t.string "uuid", null: false + t.string "uid", null: false t.string "secret", null: false - t.string "redirect_uri", null: false - t.integer "owner_id" + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false + t.boolean "confidential", default: true, null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end create_table "pull_requests", force: :cascade do |t| @@ -54,36 +77,47 @@ t.integer "user_id", null: false t.datetime "created_at" t.integer "number" + t.index ["repository_id"], name: "index_pull_requests_on_repository_id" + t.index ["user_id"], name: "index_pull_requests_on_user_id" end create_table "refs", force: :cascade do |t| t.string "name", null: false t.integer "type", null: false t.integer "repository_id", null: false + t.index ["repository_id", "type", "name"], name: "index_refs_on_repository_id_and_type_and_name", unique: true end create_table "repositories", force: :cascade do |t| t.string "name", null: false t.string "url", null: false t.integer "server_provider_id", null: false + t.datetime "last_synced_at" + t.index ["server_provider_id", "name"], name: "index_repositories_on_server_provider_id_and_name", unique: true end create_table "repository_permissions", force: :cascade do |t| t.integer "user_id", null: false t.integer "repository_id", null: false t.integer "permission", null: false + t.index ["repository_id"], name: "index_repository_permissions_on_repository_id" + t.index ["user_id"], name: "index_repository_permissions_on_user_id" end create_table "server_provider_permissions", force: :cascade do |t| t.integer "user_id", null: false t.integer "server_provider_id", null: false t.integer "permission", null: false + t.index ["server_provider_id"], name: "index_server_provider_permissions_on_server_provider_id" + t.index ["user_id"], name: "index_server_provider_permissions_on_user_id" end create_table "server_provider_user_settings", force: :cascade do |t| + t.string "username", null: false t.string "value", null: false t.integer "server_provider_user_id", null: false t.boolean "is_syncing" + t.index ["server_provider_user_id"], name: "index_server_provider_user_settings_on_server_provider_user_id" end create_table "server_providers", force: :cascade do |t| @@ -91,6 +125,8 @@ t.string "url", null: false t.string "type", null: false t.string "listener_token" + t.index ["listener_token"], name: "index_server_providers_on_listener_token", unique: true + t.index ["type", "url"], name: "index_server_providers_on_type_and_url", unique: true end create_table "settings", force: :cascade do |t| @@ -106,20 +142,47 @@ create_table "users", force: :cascade do |t| t.string "name" - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false + t.string "email", null: false + t.string "encrypted_password", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.string "encrypted_otp_secret" + t.string "encrypted_otp_secret_iv" + t.string "encrypted_otp_secret_salt" + t.integer "consumed_timestep" + t.boolean "otp_required_for_login" + t.string "otp_backup_codes", array: true + t.string "jti" t.string "confirmation_token" t.datetime "confirmation_sent_at" - t.string "type", null: false + t.datetime "confirmed_at" + t.string "unconfirmed_email" + t.string "type", default: "", null: false t.datetime "created_at", null: false + t.boolean "active", default: true t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true + t.index ["jti"], name: "index_users_on_jti", unique: true + end + + create_table "webhooks", force: :cascade do |t| + t.bigint "repository_id" + t.string "name" + t.string "url" + t.boolean "active" + t.boolean "insecure_ssl" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["repository_id"], name: "index_webhooks_on_repository_id" end add_foreign_key "commits", "refs" add_foreign_key "commits", "repositories" add_foreign_key "commits", "users" - add_foreign_key "oauth_applications", "users", column: "owner_id" + add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_grants", "users", column: "resource_owner_id" + add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_tokens", "users", column: "resource_owner_id" add_foreign_key "pull_requests", "commits", column: "base_id" add_foreign_key "pull_requests", "commits", column: "head_id" add_foreign_key "pull_requests", "repositories" @@ -131,4 +194,5 @@ add_foreign_key "server_provider_permissions", "server_providers" add_foreign_key "server_provider_permissions", "users" add_foreign_key "server_provider_user_settings", "server_provider_permissions", column: "server_provider_user_id" + add_foreign_key "webhooks", "repositories" end diff --git a/spec/controllers/v1/repositories/branches_controller_spec.rb b/spec/controllers/v1/repositories/branches_controller_spec.rb new file mode 100644 index 00000000..e89d879c --- /dev/null +++ b/spec/controllers/v1/repositories/branches_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe V1::Repositories::BranchesController, type: :controller do + +end diff --git a/spec/controllers/v1/repositories/commits_controller_spec.rb b/spec/controllers/v1/repositories/commits_controller_spec.rb new file mode 100644 index 00000000..f43d7ffd --- /dev/null +++ b/spec/controllers/v1/repositories/commits_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe V1::Repositories::CommitsController, type: :controller do + +end diff --git a/spec/controllers/v1/repositories/token_controller_spec.rb b/spec/controllers/v1/repositories/token_controller_spec.rb new file mode 100644 index 00000000..6dfda257 --- /dev/null +++ b/spec/controllers/v1/repositories/token_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe V1::Repositories::TokenController, type: :controller do + +end diff --git a/spec/controllers/v1/repositories/webhooks_controller_spec.rb b/spec/controllers/v1/repositories/webhooks_controller_spec.rb new file mode 100644 index 00000000..ca82a5c7 --- /dev/null +++ b/spec/controllers/v1/repositories/webhooks_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe V1::Repositories::WebhooksController, type: :controller do + +end diff --git a/spec/controllers/v1/repositories_controller_spec.rb b/spec/controllers/v1/repositories_controller_spec.rb new file mode 100644 index 00000000..69d1ba33 --- /dev/null +++ b/spec/controllers/v1/repositories_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe V1::RepositoriesController, type: :controller do + +end diff --git a/spec/controllers/v1/server_providers_controller_spec.rb b/spec/controllers/v1/server_providers_controller_spec.rb new file mode 100644 index 00000000..5d52e959 --- /dev/null +++ b/spec/controllers/v1/server_providers_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe V1::ServerProvidersController, type: :controller do + +end diff --git a/spec/controllers/v1/users/confirmations_controller_spec.rb b/spec/controllers/v1/users/confirmations_controller_spec.rb new file mode 100644 index 00000000..7fe9cdcf --- /dev/null +++ b/spec/controllers/v1/users/confirmations_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe V1::Users::ConfirmationsController, type: :controller do + +end diff --git a/spec/controllers/v1/users/registrations_controller_spec.rb b/spec/controllers/v1/users/registrations_controller_spec.rb new file mode 100644 index 00000000..501b5fce --- /dev/null +++ b/spec/controllers/v1/users/registrations_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe V1::Users::RegistrationsController, type: :controller do + +end diff --git a/spec/controllers/v1/users/sessions_controller_spec.rb b/spec/controllers/v1/users/sessions_controller_spec.rb new file mode 100644 index 00000000..6ff8c480 --- /dev/null +++ b/spec/controllers/v1/users/sessions_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe V1::Users::SessionsController, type: :controller do + +end diff --git a/spec/controllers/v1/users/two_factor_auth_controller_spec.rb b/spec/controllers/v1/users/two_factor_auth_controller_spec.rb new file mode 100644 index 00000000..973c6b89 --- /dev/null +++ b/spec/controllers/v1/users/two_factor_auth_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe V1::Users::TwoFactorAuthController, type: :controller do + +end diff --git a/spec/controllers/v1/users_controller_spec.rb b/spec/controllers/v1/users_controller_spec.rb new file mode 100644 index 00000000..13a96e1b --- /dev/null +++ b/spec/controllers/v1/users_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe V1::UsersController, type: :controller do + +end diff --git a/spec/controllers/v1/webhooks_controller_spec.rb b/spec/controllers/v1/webhooks_controller_spec.rb new file mode 100644 index 00000000..f4934bb3 --- /dev/null +++ b/spec/controllers/v1/webhooks_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe V1::WebhooksController, type: :controller do + +end diff --git a/spec/factories.rb b/spec/factories.rb new file mode 100644 index 00000000..1de064fe --- /dev/null +++ b/spec/factories.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :user do + name { 'Bob Uncle' } + email { 'bob@uncle.com' } + password { 'TestPass#123' } + end + + factory :server_provider do + name { 'TestServer' } + url { 'http://test.com/server' } + type { 'P4ServerProvider' } + end + + factory :server_provider_permission do + association :user + association :server_provider + permission { :owner } + end + + factory :repository do + name { 'TestRepo' } + url { 'http://test.com/repo' } + association :server_provider + last_synced_at { Time.now } + end + + factory :repository_permission do + association :user + association :repository + permission { :super } + end +end diff --git a/spec/jobs/sync_job_spec.rb b/spec/jobs/sync_job_spec.rb deleted file mode 100644 index 0e35afe1..00000000 --- a/spec/jobs/sync_job_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe SyncJob, type: :job do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/jwt_deny_list_spec.rb b/spec/models/jwt_deny_list_spec.rb deleted file mode 100644 index b89aa79b..00000000 --- a/spec/models/jwt_deny_list_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe JwtDenyList, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/p4_service_provider_spec.rb b/spec/models/p4_service_provider_spec.rb new file mode 100644 index 00000000..1b561863 --- /dev/null +++ b/spec/models/p4_service_provider_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe P4ServerProvider, type: :model do + +end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb new file mode 100644 index 00000000..1c96a561 --- /dev/null +++ b/spec/models/repository_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Repository, type: :model do + +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 47a31bb4..ac11e1ee 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,5 +1,50 @@ require 'rails_helper' RSpec.describe User, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + subject { FactoryBot.create(:user) } + + let(:server_provider) { FactoryBot.create(:server_provider) } + + context 'server_providers' do + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: subject) } + + describe '#server_provider_permission' do + it 'returns permissions for specified server provider' do + result = subject.server_provider_permission(server_provider.id) + + expect(result).to eq(server_provider_permission) + end + end + + describe '#set_server_provider_permission' do + it 'sets permissions for specified server provider' do + subject.set_server_provider_permission(server_provider.id, :member) + + expect(server_provider_permission.reload.permission).to eq('member') + end + end + end + + context 'repositories' do + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: subject) } + + describe '#repository_permission' do + it 'returns permissions for specified repository' do + result = subject.repository_permission(repository.id) + + expect(result).to eq(repository_permission) + end + end + end + + describe '#mark_as_deleted' do + it 'obfuscates user email, name and marks inactive' do + subject.mark_as_deleted + + expect(subject.reload.email).to include('deleted_email_') + expect(subject.name).to eq(nil) + expect(subject.active).to be_falsey + end + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 00000000..6ccdcef8 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,88 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV.delete('DATABASE_URL') +ENV['RACK_ENV'] = ENV['RAILS_ENV'] = ENV['ENV'] = 'test' +require File.expand_path('../config/environment', __dir__) +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +require 'database_cleaner/active_record' + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end +RSpec.configure do |config| + # add Devise methods + config.include Devise::Test::ControllerHelpers, type: :controller + config.include Devise::Test::IntegrationHelpers, type: :request + + # add `FactoryBot` methods + config.include FactoryBot::Syntax::Methods + + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + config.before(:suite) do + FactoryBot.find_definitions + DatabaseCleaner.strategy = :transaction + DatabaseCleaner.clean_with(:truncation) + end + + config.before :each do + DatabaseCleaner.start + end + + config.after :each do + DatabaseCleaner.clean + end + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..ce33d66d --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,96 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end From 917303fbb52c09d5795287f69be005c6d9e6b734 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Thu, 9 Sep 2021 13:35:07 +0300 Subject: [PATCH 42/65] Rubocop fixes --- .rubocop.yml | 29 ++- Gemfile | 23 +- Rakefile | 4 +- app/controllers/application_controller.rb | 8 +- .../concerns/paginated_collection.rb | 10 +- app/controllers/home_controller.rb | 1 - .../v1/repositories/branches_controller.rb | 36 +-- .../v1/repositories/commits_controller.rb | 44 ++-- .../v1/repositories/token_controller.rb | 54 +++-- .../v1/repositories/webhooks_controller.rb | 92 +++---- app/controllers/v1/repositories_controller.rb | 46 ++-- .../v1/server_providers_controller.rb | 224 +++++++++--------- .../v1/users/confirmations_controller.rb | 42 ++-- .../v1/users/registrations_controller.rb | 78 +++--- .../v1/users/sessions_controller.rb | 46 ++-- .../v1/users/two_factor_auth_controller.rb | 64 ++--- app/controllers/v1/users_controller.rb | 140 +++++------ app/controllers/v1/webhooks_controller.rb | 30 +-- app/helpers/application_helper.rb | 2 + app/jobs/application_job.rb | 2 + app/jobs/sync_job.rb | 2 + app/jobs/webhook_trigger_job.rb | 2 + app/mailers/application_mailer.rb | 2 + app/mailers/feedback_mailer.rb | 4 +- app/models/application_record.rb | 2 + app/models/commit.rb | 2 +- app/models/concerns/encrypted_token.rb | 2 +- app/models/concerns/p4_host_settings.rb | 2 +- app/models/ref.rb | 2 +- app/models/repository_permission.rb | 2 +- app/models/server_provider_permission.rb | 2 +- app/models/server_provider_user_setting.rb | 1 + app/models/user.rb | 20 +- app/models/webhook.rb | 2 +- app/serializers/application_serializer.rb | 2 +- app/serializers/full_ref_serializer.rb | 2 +- app/serializers/server_provider_serializer.rb | 8 +- app/serializers/user_serializer.rb | 8 +- .../authenticate_user_with_server_provider.rb | 4 +- app/services/trigger_webhooks.rb | 16 +- app/services/update_repository_credentials.rb | 4 +- app/services/validate_p4_credentials.rb | 8 +- config.ru | 4 +- lib/travis/vcs_proxy/repositories/p4.rb | 14 +- lib/travis/vcs_proxy/sync/repository.rb | 9 +- lib/travis/vcs_proxy/sync/server_provider.rb | 1 + .../repositories/branches_controller_spec.rb | 4 +- .../repositories/commits_controller_spec.rb | 4 +- .../v1/repositories/token_controller_spec.rb | 4 +- .../repositories/webhooks_controller_spec.rb | 4 +- .../v1/repositories_controller_spec.rb | 4 +- .../v1/server_providers_controller_spec.rb | 4 +- .../v1/users/confirmations_controller_spec.rb | 4 +- .../v1/users/registrations_controller_spec.rb | 4 +- .../v1/users/sessions_controller_spec.rb | 4 +- .../users/two_factor_auth_controller_spec.rb | 4 +- spec/controllers/v1/users_controller_spec.rb | 4 +- .../v1/webhooks_controller_spec.rb | 4 +- spec/models/p4_server_provider_spec.rb | 7 + spec/models/p4_service_provider_spec.rb | 5 - spec/models/repository_spec.rb | 4 +- spec/models/user_spec.rb | 10 +- spec/models/webhook_spec.rb | 2 + 63 files changed, 657 insertions(+), 521 deletions(-) create mode 100644 spec/models/p4_server_provider_spec.rb delete mode 100644 spec/models/p4_service_provider_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 0a082bd3..25ebeeb1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,20 +8,24 @@ AllCops: - lib/tasks/* - bin/* - rgloader/* - TargetRubyVersion: 2.6 + - db/* + - config/**/* + - spec/rails_helper.rb + - spec/spec_helper.rb + TargetRubyVersion: 2.7 Style/Documentation: Enabled: false +Metrics/ClassLength: + Enabled: false + Metrics/LineLength: - Exclude: - - config/initializers/* - Enabled: true - Max: 120 + Enabled: false Metrics/MethodLength: CountComments: false - Max: 15 + Max: 40 Exclude: - spec/**/*.rb @@ -30,12 +34,25 @@ Metrics/BlockLength: - spec/**/*.rb - config/routes.rb +Metrics/AbcSize: + Enabled: false + Style/TrailingCommaInArrayLiteral: EnforcedStyleForMultiline: comma Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: comma +Style/StringLiterals: + Exclude: + - db/schema.rb + +Style/IfUnlessModifier: + Enabled: false + +Lint/AssignmentInCondition: + Enabled: false + RSpec/DescribedClass: EnforcedStyle: explicit diff --git a/Gemfile b/Gemfile index 7b42cfa0..c9f82e24 100644 --- a/Gemfile +++ b/Gemfile @@ -1,38 +1,39 @@ +# frozen_string_literal: true + source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '3.0.1' -gem 'rails', '~> 6.1.3', '>= 6.1.3.2' +gem 'bootsnap', require: false +gem 'config' gem 'devise' gem 'devise-jwt' gem 'devise-two-factor' gem 'doorkeeper' +gem 'jsonapi-serializer' +gem 'kaminari' gem 'ledermann-rails-settings' +gem 'p4ruby' gem 'pg' -gem 'redis' gem 'puma', '~> 5.0' gem 'rack-cors' -gem 'config' -gem 'jsonapi-serializer' -gem 'p4ruby' +gem 'rails', '~> 6.1.3', '>= 6.1.3.2' +gem 'redis' gem 'sidekiq' -gem 'kaminari' gem 'validate_url' -gem 'bootsnap', require: false - group :development, :test do gem 'brakeman' - gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + gem 'byebug', platforms: %i[mri mingw x64_mingw] gem 'factory_bot' - gem 'rspec-rails' gem 'listen' + gem 'rspec-rails' end group :test do - gem 'rspec' gem 'database_cleaner' + gem 'rspec' end group :development do diff --git a/Rakefile b/Rakefile index a9a8a3a8..3bf36a4a 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require_relative "config/application" +require_relative 'config/application' Rails.application.load_tasks diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2da5614e..a3cff5b8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + class ApplicationController < ActionController::API before_action :require_2fa private def require_authentication - head :unauthorized and return unless user_signed_in? + head :unauthorized && return unless user_signed_in? end def require_2fa - head :unauthorized and return unless two_factor_auth_enabled? + head :unauthorized && return unless two_factor_auth_enabled? end def two_factor_auth_enabled? @@ -16,7 +18,7 @@ def two_factor_auth_enabled? end def presented_entity(resource_name, resource) - "#{resource_name.to_s.classify}Serializer".constantize.new(resource, { params: { current_user: current_user } }).to_h + "#{resource_name.to_s.classify}Serializer".constantize.new(resource, params: { current_user: current_user }).to_h end def current_user_jwt_token diff --git a/app/controllers/concerns/paginated_collection.rb b/app/controllers/concerns/paginated_collection.rb index 2346768c..87d48c4e 100644 --- a/app/controllers/concerns/paginated_collection.rb +++ b/app/controllers/concerns/paginated_collection.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module PaginatedCollection extend ActiveSupport::Concern @@ -12,14 +14,14 @@ def paginated_collection(resource_name, serializer, collection) limit: limit, first: { limit: limit, - offset: 0 + offset: 0, }, last: { limit: limit, - offset: (collection.total_pages - 1) * limit + offset: (collection.total_pages - 1) * limit, }, prev: nil, - next: nil + next: nil, } pagination[:prev] = { limit: limit, offset: (collection.prev_page - 1) * limit } if collection.prev_page pagination[:next] = { limit: limit, offset: collection.current_page * limit } if collection.next_page @@ -31,4 +33,4 @@ def paginated_collection(resource_name, serializer, collection) data end -end \ No newline at end of file +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 0b01f0ac..ce8a95e8 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -5,4 +5,3 @@ def index head :ok end end - diff --git a/app/controllers/v1/repositories/branches_controller.rb b/app/controllers/v1/repositories/branches_controller.rb index cf1b2f61..fe95c06f 100644 --- a/app/controllers/v1/repositories/branches_controller.rb +++ b/app/controllers/v1/repositories/branches_controller.rb @@ -1,25 +1,29 @@ # frozen_string_literal: true -class V1::Repositories::BranchesController < ApplicationController - before_action :require_authentication - before_action :set_repository - before_action :set_branch, only: [:show] +module V1 + module Repositories + class BranchesController < ApplicationController + before_action :require_authentication + before_action :set_repository + before_action :set_branch, only: [:show] - def index - render json: @repository.branches.map { |branch| presented_entity(:ref, branch) } - end + def index + render json: @repository.branches.map { |branch| presented_entity(:ref, branch) } + end - def show - render json: presented_entity(:ref, @branch) - end + def show + render json: presented_entity(:ref, @branch) + end - private + private - def set_repository - @repository = current_user.repositories.find(params[:repository_id]) - end + def set_repository + @repository = current_user.repositories.find(params[:repository_id]) + end - def set_branch - @branch = @repository.branches.find(params[:id]) + def set_branch + @branch = @repository.branches.find(params[:id]) + end + end end end diff --git a/app/controllers/v1/repositories/commits_controller.rb b/app/controllers/v1/repositories/commits_controller.rb index 0d38adb6..4588435b 100644 --- a/app/controllers/v1/repositories/commits_controller.rb +++ b/app/controllers/v1/repositories/commits_controller.rb @@ -1,30 +1,34 @@ # frozen_string_literal: true -class V1::Repositories::CommitsController < ApplicationController - before_action :require_authentication - before_action :set_repository - before_action :set_branch, only: [:index] - before_action :set_commit, only: [:show] +module V1 + module Repositories + class CommitsController < ApplicationController + before_action :require_authentication + before_action :set_repository + before_action :set_branch, only: [:index] + before_action :set_commit, only: [:show] - def index - render json: @branch.commits.order('committed_at DESC').map { |commit| presented_entity(:commit, commit) } - end + def index + render json: @branch.commits.order('committed_at DESC').map { |commit| presented_entity(:commit, commit) } + end - def show - render json: presented_entity(:commit, @commit) - end + def show + render json: presented_entity(:commit, @commit) + end - private + private - def set_repository - @repository = current_user.repositories.find(params[:repository_id]) - end + def set_repository + @repository = current_user.repositories.find(params[:repository_id]) + end - def set_branch - @branch = @repository.branches.find_by!(name: params[:branch]) - end + def set_branch + @branch = @repository.branches.find_by!(name: params[:branch]) + end - def set_commit - @commit = @repository.commits.find_by!(sha: params[:id]) + def set_commit + @commit = @repository.commits.find_by!(sha: params[:id]) + end + end end end diff --git a/app/controllers/v1/repositories/token_controller.rb b/app/controllers/v1/repositories/token_controller.rb index db7e2fc0..35a9521b 100644 --- a/app/controllers/v1/repositories/token_controller.rb +++ b/app/controllers/v1/repositories/token_controller.rb @@ -1,39 +1,43 @@ # frozen_string_literal: true -class V1::Repositories::TokenController < ApplicationController - before_action :require_authentication - before_action :set_repository +module V1 + module Repositories + class TokenController < ApplicationController + before_action :require_authentication + before_action :set_repository - def update - head :bad_request and return if params[:username].blank? || params[:token].blank? + def update # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + head(:bad_request) && return if params[:username].blank? || params[:token].blank? - permission = current_user.repository_permission(@repository.id) - head :forbidden and return if permission.blank? || (!permission.owner? && !permission.admin?) + permission = current_user.repository_permission(@repository.id) + head(:forbidden) && return if permission.blank? || (!permission.owner? && !permission.admin?) - success = false - begin - success = UpdateRepositoryCredentials.new(@repository, params[:username], params[:token]).call - rescue UpdateRepositoryCredentials::ValidationFailed - render json: { errors: [ 'Cannot authenticate' ] }, status: :unprocessable_entity and return - end + success = false + begin + success = UpdateRepositoryCredentials.new(@repository, params[:username], params[:token]).call + rescue UpdateRepositoryCredentials::ValidationFailed + render(json: { errors: ['Cannot authenticate'] }, status: :unprocessable_entity) && (return) + end - head :ok and return if success + head(:ok) && return if success - render json: { errors: @repository.errors }, status: :unprocessable_entity - end + render json: { errors: @repository.errors }, status: :unprocessable_entity + end - def destroy - permission = current_user.repository_permission(@repository.id) - head :forbidden and return if permission.blank? || (!permission.owner? && !permission.admin?) + def destroy # rubocop:disable Metrics/CyclomaticComplexity + permission = current_user.repository_permission(@repository.id) + head(:forbidden) && return if permission.blank? || (!permission.owner? && !permission.admin?) - head :ok and return if UpdateRepositoryCredentials.new(@repository, nil, nil).call + head(:ok) && return if UpdateRepositoryCredentials.new(@repository, nil, nil).call - render json: { errors: @repository.errors }, status: :unprocessable_entity - end + render json: { errors: @repository.errors }, status: :unprocessable_entity + end - private + private - def set_repository - @repository = current_user.repositories.find_by(id: params[:repository_id]) + def set_repository + @repository = current_user.repositories.find_by(id: params[:repository_id]) + end + end end end diff --git a/app/controllers/v1/repositories/webhooks_controller.rb b/app/controllers/v1/repositories/webhooks_controller.rb index 8802e6f9..de88e5d6 100644 --- a/app/controllers/v1/repositories/webhooks_controller.rb +++ b/app/controllers/v1/repositories/webhooks_controller.rb @@ -1,48 +1,52 @@ # frozen_string_literal: true -class V1::Repositories::WebhooksController < ApplicationController - before_action :require_authentication - before_action :set_repository - before_action :set_webhook, only: [:show, :update] - - def index - render json: @repository.webhooks.map { |webhook| presented_entity(:webhook, webhook) } - end - - def show - render json: presented_entity(:webhook, @webhook) - end - - def create - webhook = @repository.webhooks.build(webhook_params) - if webhook.save - render json: presented_entity(:webhook, webhook) - return - end - - render json: { errors: webhook.errors.full_messages }, status: :unprocessable_entity - end - - def update - if @webhook.update(webhook_params) - render json: presented_entity(:webhook, @webhook) - return +module V1 + module Repositories + class WebhooksController < ApplicationController + before_action :require_authentication + before_action :set_repository + before_action :set_webhook, only: %i[show update] + + def index + render json: @repository.webhooks.map { |webhook| presented_entity(:webhook, webhook) } + end + + def show + render json: presented_entity(:webhook, @webhook) + end + + def create + webhook = @repository.webhooks.build(webhook_params) + if webhook.save + render json: presented_entity(:webhook, webhook) + return + end + + render json: { errors: webhook.errors.full_messages }, status: :unprocessable_entity + end + + def update + if @webhook.update(webhook_params) + render json: presented_entity(:webhook, @webhook) + return + end + + render json: { errors: webhook.errors.full_messages }, status: :unprocessable_entity + end + + private + + def set_repository + @repository = current_user.repositories.find(params[:repository_id]) + end + + def set_webhook + @webhook = @repository.webhooks.find(params[:id]) + end + + def webhook_params + params.require(:webhook).permit(:name, :url, :active, :insecure_ssl) + end end - - render json: { errors: webhook.errors.full_messages }, status: :unprocessable_entity - end - - private - - def set_repository - @repository = current_user.repositories.find(params[:repository_id]) - end - - def set_webhook - @webhook = @repository.webhooks.find(params[:id]) - end - - def webhook_params - params.require(:webhook).permit(:name, :url, :active, :insecure_ssl) end -end \ No newline at end of file +end diff --git a/app/controllers/v1/repositories_controller.rb b/app/controllers/v1/repositories_controller.rb index beb700ef..fdd3d508 100644 --- a/app/controllers/v1/repositories_controller.rb +++ b/app/controllers/v1/repositories_controller.rb @@ -1,35 +1,37 @@ # frozen_string_literal: true -class V1::RepositoriesController < ApplicationController - before_action :require_authentication - before_action :set_repository +module V1 + class RepositoriesController < ApplicationController + before_action :require_authentication + before_action :set_repository - def show - render json: presented_entity(:repository, @repository) - end + def show + render json: presented_entity(:repository, @repository) + end - def refs - render json: @repository.refs.map { |ref| presented_entity(:full_ref, ref) } - end + def refs + render json: @repository.refs.map { |ref| presented_entity(:full_ref, ref) } + end - def content - head :bad_request and return if params[:ref].blank? || params[:path].blank? + def content + head(:bad_request) && return if params[:ref].blank? || params[:path].blank? - result = @repository.file_contents(params[:ref], params[:path]) - render json: { errors: [ 'Cannot render file' ] }, status: :unprocessable_entity and return if result.blank? + result = @repository.file_contents(params[:ref], params[:path]) + render(json: { errors: ['Cannot render file'] }, status: :unprocessable_entity) && return if result.blank? - render plain: result[1] - end + render plain: result[1] + end - def sync - SyncJob.perform_later(SyncJob::SyncType::REPOSITORY, @repository.id, current_user.id) + def sync + SyncJob.perform_later(SyncJob::SyncType::REPOSITORY, @repository.id, current_user.id) - head :ok - end + head :ok + end - private + private - def set_repository - @repository = current_user.repositories.includes(:permissions).find(params[:id]) + def set_repository + @repository = current_user.repositories.includes(:permissions).find(params[:id]) + end end end diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index ad4e25d0..1013b0ee 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -1,160 +1,164 @@ # frozen_string_literal: true -class V1::ServerProvidersController < ApplicationController - include PaginatedCollection +module V1 + class ServerProvidersController < ApplicationController + include PaginatedCollection - before_action :require_authentication - before_action :set_server_provider, only: [:show, :update, :authenticate, :forget, :repositories, :sync] + before_action :require_authentication + before_action :set_server_provider, only: %i[show update authenticate forget repositories sync] - PROVIDER_KLASS = { - 'perforce' => P4ServerProvider - }.freeze + PROVIDER_KLASS = { + 'perforce' => P4ServerProvider, + }.freeze - def create - head :bad_request and return if params[:server_provider].blank? || !PROVIDER_KLASS.has_key?(params[:server_provider][:type]) - - klass = PROVIDER_KLASS[params[:server_provider][:type]] - render json: { errors: [ 'A server with this URL already exists.' ] }, status: :unprocessable_entity and return if klass.find_by(url: params[:server_provider][:url]).present? + def create # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + if params[:server_provider].blank? || !PROVIDER_KLASS.key?(params[:server_provider][:type]) + head(:bad_request) && return + end - errors = [] - provider = nil - ActiveRecord::Base.transaction do - provider = klass.new(server_provider_params) - unless provider.save - errors = provider.errors - raise ActiveRecord::Rollback + klass = PROVIDER_KLASS[params[:server_provider][:type]] + if klass.find_by(url: params[:server_provider][:url]).present? + render(json: { errors: ['A server with this URL already exists.'] }, status: :unprocessable_entity) && return end - set_provider_credentials(provider, errors) + errors = [] + provider = nil + ActiveRecord::Base.transaction do + provider = klass.new(server_provider_params) + unless provider.save + errors = provider.errors + raise ActiveRecord::Rollback + end - unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission.permissions[:owner]) - errors << 'Cannot set permission for user' - raise ActiveRecord::Rollback + set_provider_credentials(provider, errors) + + unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission.permissions[:owner]) + errors << 'Cannot set permission for user' + raise ActiveRecord::Rollback + end end - end - render json: presented_entity(:server_provider, provider) and return if errors.blank? + render(json: presented_entity(:server_provider, provider)) && return if errors.blank? - render json: { errors: errors }, status: :unprocessable_entity - end + render json: { errors: errors }, status: :unprocessable_entity + end - def show - render json: presented_entity(:server_provider, @server_provider) - end + def show + render json: presented_entity(:server_provider, @server_provider) + end - def update - permission = current_user.server_provider_permission(@server_provider.id) - head :forbidden and return if permission.blank? || !permission.owner? + def update # rubocop:disable Metrics/CyclomaticComplexity + permission = current_user.server_provider_permission(@server_provider.id) + head(:forbidden) && return if permission.blank? || !permission.owner? - errors = [] - ActiveRecord::Base.transaction do - unless @server_provider.update(server_provider_params) - errors = @server_provider.errors - raise ActiveRecord::Rollback - end + errors = [] + ActiveRecord::Base.transaction do + unless @server_provider.update(server_provider_params) + errors = @server_provider.errors + raise ActiveRecord::Rollback + end - set_provider_credentials(@server_provider, errors) - end + set_provider_credentials(@server_provider, errors) + end - head :ok and return if errors.blank? + head(:ok) && return if errors.blank? - render json: { errors: errors }, status: :unprocessable_entity - end + render json: { errors: errors }, status: :unprocessable_entity + end - def authenticate - head :bad_request and return if params[:token].blank? || params[:username].blank? + def authenticate # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + head(:bad_request) && return if params[:token].blank? || params[:username].blank? + + success = true + ActiveRecord::Base.transaction do + permission = current_user.server_provider_permissions.find_or_initialize_by(server_provider_id: @server_provider.id) + unless permission.persisted? + permission.permission = ServerProviderPermission.permissions[:member] + unless permission.save + success = false + raise ActiveRecord::Rollback + end + end - success = true - ActiveRecord::Base.transaction do - permission = current_user.server_provider_permissions.find_or_initialize_by(server_provider_id: @server_provider.id) - unless permission.persisted? - permission.permission = ServerProviderPermission.permissions[:member] - unless permission.save + unless AuthenticateUserWithServerProvider.new(permission, @server_provider, params[:username], params[:token]).call success = false raise ActiveRecord::Rollback end end - unless AuthenticateUserWithServerProvider.new(permission, @server_provider, params[:username], params[:token]).call - success = false - raise ActiveRecord::Rollback - end + head(:ok) && return if success + + render json: { errors: ['Cannot authenticate'] }, status: :unprocessable_entity end - head :ok and return if success + def forget + current_user.server_provider_permission(@server_provider.id)&.destroy - render json: { errors: [ 'Cannot authenticate' ] }, status: :unprocessable_entity - end + head :ok + end - def forget - current_user.server_provider_permission(@server_provider.id)&.destroy + def sync + SyncJob.perform_later(SyncJob::SyncType::SERVER_PROVIDER, @server_provider.id, current_user.id) - head :ok - end - - def sync - SyncJob.perform_later(SyncJob::SyncType::SERVER_PROVIDER, @server_provider.id, current_user.id) + head :ok + end - head :ok - end + def repositories + order = params[:sort_by] == 'last_synced_at' ? 'DESC' : 'ASC' + repositories = @server_provider.repositories + .includes(:permissions) + .includes(:setting_objects) + repositories = repositories.order(params[:sort_by] => order) if params[:sort_by].present? + repositories = repositories.where('name LIKE ?', "%#{params[:filter]}%") if params[:filter].present? - def repositories - order = params[:sort_by] == 'last_synced_at' ? 'DESC' : 'ASC' - repositories = @server_provider.repositories - .includes(:permissions) - .includes(:setting_objects) - repositories = repositories.order(params[:sort_by] => order) if params[:sort_by].present? - if params[:filter].present? - repositories = repositories.where('name LIKE ?', "%#{params[:filter]}%") + render json: paginated_collection(:repositories, :repository, repositories.page(params[:page]).per(params[:limit])) end - render json: paginated_collection(:repositories, :repository, repositories.page(params[:page]).per(params[:limit])) - end + def by_url + head(:bad_request) && return if params[:url].blank? - def by_url - head :bad_request and return if params[:url].blank? + render json: presented_entity(:server_provider, ServerProvider.find_by!(url: params[:url])) + end - render json: presented_entity(:server_provider, ServerProvider.find_by!(url: params[:url])) - end + def add_by_url + head(:bad_request) && return if params[:url].blank? - def add_by_url - head :bad_request and return if params[:url].blank? + errors = [] + provider = ServerProvider.find_by!(url: params[:url]) - errors = [] - provider = ServerProvider.find_by!(url: params[:url]) + unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission.permissions[:member]) + errors << 'Cannot set permission for user' + raise ActiveRecord::Rollback + end - unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission.permissions[:member]) - errors << 'Cannot set permission for user' - raise ActiveRecord::Rollback - end + render(json: presented_entity(:server_provider, provider)) && return if errors.blank? - render json: presented_entity(:server_provider, provider) and return if errors.blank? + render json: { errors: errors }, status: :unprocessable_entity + end - render json: { errors: errors }, status: :unprocessable_entity - end + private - private + def server_provider_params + params.require(:server_provider).permit(:name, :url) + end - def server_provider_params - params.require(:server_provider).permit(:name, :url) - end + def set_server_provider + @server_provider = ServerProvider.includes(:server_provider_permissions).find(params[:id]) + end - def set_server_provider - @server_provider = ServerProvider.includes(:server_provider_permissions).find(params[:id]) - end + def set_provider_credentials(provider, errors) + return if params[:server_provider][:username].blank? || params[:server_provider][:token].blank? - def set_provider_credentials(provider, errors) - return if params[:server_provider][:username].blank? || params[:server_provider][:token].blank? + success = false + begin + success = UpdateRepositoryCredentials.new(provider, params[:server_provider][:username], params[:server_provider][:token]) + rescue UpdateRepositoryCredentials::ValidationFailed + errors << 'Cannot authenticate' + raise ActiveRecord::Rollback + end - success = false - begin - success = UpdateRepositoryCredentials.new(provider, params[:server_provider][:username], params[:server_provider][:token]) - rescue UpdateRepositoryCredentials::ValidationFailed => e - errors << 'Cannot authenticate' - raise ActiveRecord::Rollback - end + return if success - unless success errors << 'Cannot save credentials' raise ActiveRecord::Rollback end diff --git a/app/controllers/v1/users/confirmations_controller.rb b/app/controllers/v1/users/confirmations_controller.rb index 5dcc62bf..6823bbd1 100644 --- a/app/controllers/v1/users/confirmations_controller.rb +++ b/app/controllers/v1/users/confirmations_controller.rb @@ -1,29 +1,33 @@ # frozen_string_literal: true -class V1::Users::ConfirmationsController < Devise::ConfirmationsController - clear_respond_to - respond_to :json - skip_before_action :require_2fa +module V1 + module Users + class ConfirmationsController < Devise::ConfirmationsController + clear_respond_to + respond_to :json + skip_before_action :require_2fa - def show - user = User.confirm_by_token(params[:confirmation_token]) - if user.errors.present? - redirect_uri = URI.join(Settings.web_url, 'unconfirmed') - redirect_uri.query = 'error=expired' - redirect_to redirect_uri.to_s and return - end + def show + user = User.confirm_by_token(params[:confirmation_token]) + if user.errors.present? + redirect_uri = URI.join(Settings.web_url, 'unconfirmed') + redirect_uri.query = 'error=expired' + redirect_to(redirect_uri.to_s) && return + end - redirect_to URI.join(Settings.web_url, 'confirmed').to_s - end + redirect_to URI.join(Settings.web_url, 'confirmed').to_s + end - def resend - head :bad_request and return if params[:email].blank? + def resend + head(:bad_request) && return if params[:email].blank? - user = User.find_by(email: params[:email]) - head :ok and return if user.blank? || user.confirmed? + user = User.find_by(email: params[:email]) + head(:ok) && return if user.blank? || user.confirmed? - user.resend_confirmation_instructions + user.resend_confirmation_instructions - head :ok + head :ok + end + end end end diff --git a/app/controllers/v1/users/registrations_controller.rb b/app/controllers/v1/users/registrations_controller.rb index 7110cd57..920ad9ec 100644 --- a/app/controllers/v1/users/registrations_controller.rb +++ b/app/controllers/v1/users/registrations_controller.rb @@ -1,41 +1,47 @@ # frozen_string_literal: true -class V1::Users::RegistrationsController < Devise::RegistrationsController - clear_respond_to - respond_to :json - skip_before_action :require_2fa - - def create - build_resource(sign_up_params) - - resource.save - if resource.persisted? - expire_data_after_sign_in! - head :ok - return +module V1 + module Users + class RegistrationsController < Devise::RegistrationsController + clear_respond_to + respond_to :json + skip_before_action :require_2fa + + def create + build_resource(sign_up_params) + + resource.save + if resource.persisted? + expire_data_after_sign_in! + head :ok + return + end + + render json: { errors: resource.errors }, status: :unprocessable_entity + end + + def update + head :not_found + end + + def destroy + unless resource.valid_password?(params[:password]) + render(json: { errors: ['Invalid credentials'] }, status: :unprocessable_entity) && return + end + + if params[:feedback].present? + permitted = params.require(:feedback).permit(:reason, :text) + FeedbackMailer.with(email: resource.email, feedback: permitted).send_feedback.deliver_now + end + + resource.mark_as_deleted + sign_out + head :ok + end + + def cancel + head :not_found + end end - - render json: { errors: resource.errors }, status: :unprocessable_entity - end - - def update - head :not_found - end - - def destroy - render json: { errors: [ 'Invalid credentials' ] }, status: :unprocessable_entity and return unless resource.valid_password?(params[:password]) - - if params[:feedback].present? - permitted = params.require(:feedback).permit(:reason, :text) - FeedbackMailer.with(email: resource.email, feedback: permitted).send_feedback.deliver_now - end - - resource.mark_as_deleted - sign_out - head :ok - end - - def cancel - head :not_found end end diff --git a/app/controllers/v1/users/sessions_controller.rb b/app/controllers/v1/users/sessions_controller.rb index 6e1c162a..f25924ec 100644 --- a/app/controllers/v1/users/sessions_controller.rb +++ b/app/controllers/v1/users/sessions_controller.rb @@ -1,29 +1,37 @@ # frozen_string_literal: true -class V1::Users::SessionsController < Devise::SessionsController - clear_respond_to - respond_to :json - skip_before_action :require_2fa +module V1 + module Users + class SessionsController < Devise::SessionsController + clear_respond_to + respond_to :json + skip_before_action :require_2fa - def create - head :bad_request and return if !params.has_key?(:user) || params[:user][:email].blank? || params[:user][:password].blank? - user = User.find_by(email: params[:user][:email]) - head :unauthorized and return if user.blank? - render json: { token: '', otp_enabled: true } and return if user.valid_password?(params[:user][:password]) && user.otp_required_for_login? && params[:user][:otp_attempt].blank? + def create # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + if !params.key?(:user) || params[:user][:email].blank? || params[:user][:password].blank? + head(:bad_request) && return + end + user = User.find_by(email: params[:user][:email]) + head(:unauthorized) && return if user.blank? + if user.valid_password?(params[:user][:password]) && user.otp_required_for_login? && params[:user][:otp_attempt].blank? + render(json: { token: '', otp_enabled: true }) && return + end - user = warden.authenticate!(auth_options) + user = warden.authenticate!(auth_options) - render json: { token: current_user_jwt_token, otp_enabled: user.otp_required_for_login? } - end + render json: { token: current_user_jwt_token, otp_enabled: user.otp_required_for_login? } + end - private + private - # Overriding because we don't need to send any answer - def respond_to_on_destroy - head :ok - end + # Overriding because we don't need to send any answer + def respond_to_on_destroy + head :ok + end - def configure_permitted_parameters - devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt]) + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt]) + end + end end end diff --git a/app/controllers/v1/users/two_factor_auth_controller.rb b/app/controllers/v1/users/two_factor_auth_controller.rb index 67e5a60c..022336af 100644 --- a/app/controllers/v1/users/two_factor_auth_controller.rb +++ b/app/controllers/v1/users/two_factor_auth_controller.rb @@ -1,33 +1,39 @@ # frozen_string_literal: true -class V1::Users::TwoFactorAuthController < ApplicationController - before_action :require_authentication - skip_before_action :require_2fa, only: [:url, :enable] - - def url - if current_user.otp_secret.blank? - current_user.otp_secret = User.generate_otp_secret - render json: { errors: current_user.errors }, status: :unprocessable_entity and return unless current_user.save +module V1 + module Users + class TwoFactorAuthController < ApplicationController + before_action :require_authentication + skip_before_action :require_2fa, only: %i[url enable] + + def url + if current_user.otp_secret.blank? + current_user.otp_secret = User.generate_otp_secret + render(json: { errors: current_user.errors }, status: :unprocessable_entity) && return unless current_user.save + end + + render json: { url: current_user.otp_provisioning_uri(current_user.email, issuer: 'Travis CI VCS Proxy') } + end + + def enable + unless params[:otp_attempt] == current_user.current_otp + render(json: { errors: ['Wrong OTP code'] }, status: :unprocessable_entity) && return + end + + current_user.otp_required_for_login = true + render(json: { errors: current_user.errors }, status: :unprocessable_entity) && return unless current_user.save + + User.revoke_jwt(nil, current_user) + warden.set_user(current_user) + render json: { token: current_user_jwt_token, otp_enabled: true } + end + + def codes + codes = current_user.generate_otp_backup_codes! + render json: { errors: current_user.errors }, status: :unprocessable_entity unless current_user.save + + render json: { codes: codes } + end end - - render json: { url: current_user.otp_provisioning_uri(current_user.email, issuer: 'Travis CI VCS Proxy') } - end - - def enable - render json: { errors: [ 'Wrong OTP code' ] }, status: :unprocessable_entity and return unless params[:otp_attempt] == current_user.current_otp - - current_user.otp_required_for_login = true - render json: { errors: current_user.errors }, status: :unprocessable_entity and return unless current_user.save - - User.revoke_jwt(nil, current_user) - warden.set_user(current_user) - render json: { token: current_user_jwt_token, otp_enabled: true } - end - - def codes - codes = current_user.generate_otp_backup_codes! - render json: { errors: current_user.errors }, status: :unprocessable_entity unless current_user.save - - render json: { codes: codes } end -end \ No newline at end of file +end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 7db25ba4..9bd16016 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -1,93 +1,99 @@ # frozen_string_literal: true -class V1::UsersController < ApplicationController - include PaginatedCollection +module V1 + class UsersController < ApplicationController + include PaginatedCollection - before_action :require_authentication, except: [:request_password_reset, :reset_password] - skip_before_action :require_2fa, only: [:show, :request_password_reset, :reset_password] + before_action :require_authentication, except: %i[request_password_reset reset_password] + skip_before_action :require_2fa, only: %i[show request_password_reset reset_password] - def show - render json: presented_entity(:user, current_user) - end - - def update_email - head :bad_request and return if params[:email].blank? - - current_user.email = params[:email] - if current_user.save - head :ok - return + def show + render json: presented_entity(:user, current_user) end - render json: { errors: current_user.errors }, status: :unprocessable_entity - end - - def update_password - head :bad_request and return if params[:current_password].blank? || params[:password].blank? || params[:password_confirmation].blank? + def update_email + head(:bad_request) && return if params[:email].blank? - unless current_user.valid_password?(params[:current_password]) - render json: { errors: [ 'Invalid current password' ] }, status: :unprocessable_entity - return - end + current_user.email = params[:email] + if current_user.save + head :ok + return + end - unless params[:password] == params[:password_confirmation] - render json: { errors: [ 'Password does not match confirmation' ] }, status: :unprocessable_entity - return + render json: { errors: current_user.errors }, status: :unprocessable_entity end - current_user.password = params[:password] - current_user.password_confirmation = params[:password_confirmation] - if current_user.save - head :ok - return + def update_password # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + if params[:current_password].blank? || params[:password].blank? || params[:password_confirmation].blank? + head(:bad_request) && return + end + + unless current_user.valid_password?(params[:current_password]) + render json: { errors: ['Invalid current password'] }, status: :unprocessable_entity + return + end + + unless params[:password] == params[:password_confirmation] + render json: { errors: ['Password does not match confirmation'] }, status: :unprocessable_entity + return + end + + current_user.password = params[:password] + current_user.password_confirmation = params[:password_confirmation] + if current_user.save + head :ok + return + end + + render json: { errors: current_user.errors }, status: :unprocessable_entity end - render json: { errors: current_user.errors }, status: :unprocessable_entity - end + def request_password_reset + head(:ok) && return if params[:email].blank? + head(:ok) && return unless user = User.find_by(email: params[:email]) - def request_password_reset - head :ok and return if params[:email].blank? - head :ok and return unless user = User.find_by(email: params[:email]) + user.send_reset_password_instructions - user.send_reset_password_instructions + head :ok + end - head :ok - end + def reset_password + if params[:reset_password_token].blank? || params[:password].blank? || params[:password_confirmation].blank? + head(:bad_request) && return + end - def reset_password - head :bad_request and return if params[:reset_password_token].blank? || params[:password].blank? || params[:password_confirmation].blank? + user = User.reset_password_by_token(params.slice(:reset_password_token, :password, :password_confirmation)) + if user.errors.empty? + head :ok + return + end - user = User.reset_password_by_token(params.slice(:reset_password_token, :password, :password_confirmation)) - if user.errors.empty? - head :ok - return + render json: { errors: user.errors }, status: :unprocessable_entity end - render json: { errors: user.errors }, status: :unprocessable_entity - end - - def emails - render json: { emails: [ current_user.email ] } - end + def emails + render json: { emails: [current_user.email] } + end - def server_providers - server_providers = current_user.server_providers - .includes(:server_provider_permissions) - .includes(:setting_objects) - .order(:name) - .page(params[:page]) - .per(params[:limit]) + def server_providers + server_providers = current_user.server_providers + .includes(:server_provider_permissions) + .includes(:setting_objects) + .order(:name) + .page(params[:page]) + .per(params[:limit]) - render json: paginated_collection(:server_providers, :server_provider, server_providers) - end + render json: paginated_collection(:server_providers, :server_provider, server_providers) + end - def repositories - render json: current_user.repositories.map { |repository| presented_entity(:repository, repository) } - end + def repositories + render json: current_user.repositories.map { |repository| presented_entity(:repository, repository) } + end - def sync - SyncJob.perform_later(SyncJob::SyncType::USER, current_user.id) + def sync + SyncJob.perform_later(SyncJob::SyncType::USER, current_user.id) - head :ok + head :ok + end end end diff --git a/app/controllers/v1/webhooks_controller.rb b/app/controllers/v1/webhooks_controller.rb index 83b55fbd..5ec3dd35 100644 --- a/app/controllers/v1/webhooks_controller.rb +++ b/app/controllers/v1/webhooks_controller.rb @@ -1,24 +1,26 @@ # frozen_string_literal: true -class V1::WebhooksController < ApplicationController - def receive - head :unauthorized and return unless server_provider = ServerProvider.find_by(listener_token: params[:token]) +module V1 + class WebhooksController < ApplicationController + def receive # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + head(:unauthorized) && return unless server_provider = ServerProvider.find_by(listener_token: params[:token]) - head :internal_server_error unless commit_info = server_provider.commit_info_from_webhook(params) + head :internal_server_error unless commit_info = server_provider.commit_info_from_webhook(params) - # TODO: Figure out if we should really ignore the hook if there is no user with the given email - head :ok and return unless user = server_provider.users.find_by(email: commit_info[:email]) - # TODO: Figure out if we should really ignore the hook if there is no repository with this name - head :ok and return unless repository = server_provider.repositories.find_by(name: commit_info[:repository_name]) + # TODO: Figure out if we should really ignore the hook if there is no user with the given email + head(:ok) && return unless user = server_provider.users.find_by(email: commit_info[:email]) + # TODO: Figure out if we should really ignore the hook if there is no repository with this name + head(:ok) && return unless repository = server_provider.repositories.find_by(name: commit_info[:repository_name]) - ref = repository.refs.branch.find_by(name: commit_info[:ref]) || repository.refs.branch.create(name: commit_info[:ref]) - head :unprocessable_entity and return unless ref + ref = repository.refs.branch.find_by(name: commit_info[:ref]) || repository.refs.branch.create(name: commit_info[:ref]) + head(:unprocessable_entity) && return unless ref - commit = ref.commits.find_by(sha: params[:sha]) || ref.commits.create(sha: params[:sha], repository: repository, user: user) - head :unprocessable_entity and return unless commit + commit = ref.commits.find_by(sha: params[:sha]) || ref.commits.create(sha: params[:sha], repository: repository, user: user) + head(:unprocessable_entity) && return unless commit - TriggerWebhooks.new(commit).call + TriggerWebhooks.new(commit).call - head :ok + head :ok + end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be794..15b06f0f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + module ApplicationHelper end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index d394c3d1..bef39599 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked diff --git a/app/jobs/sync_job.rb b/app/jobs/sync_job.rb index f0c5a20c..403a3de3 100644 --- a/app/jobs/sync_job.rb +++ b/app/jobs/sync_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'travis/vcs_proxy/syncer' class SyncJob < ApplicationJob diff --git a/app/jobs/webhook_trigger_job.rb b/app/jobs/webhook_trigger_job.rb index ed065f3e..0ee41bc9 100644 --- a/app/jobs/webhook_trigger_job.rb +++ b/app/jobs/webhook_trigger_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class WebhookTriggerJob < ApplicationJob queue_as :default diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index f78416ca..04bbea05 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationMailer < ActionMailer::Base default from: Settings.mail_from layout 'mailer' diff --git a/app/mailers/feedback_mailer.rb b/app/mailers/feedback_mailer.rb index 9df73df2..518c1e2c 100644 --- a/app/mailers/feedback_mailer.rb +++ b/app/mailers/feedback_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class FeedbackMailer < ApplicationMailer default from: 'from@example.com' layout 'mailer' @@ -7,4 +9,4 @@ def send_feedback @feedback = params[:feedback] mail(to: Settings.feedback_mail, subject: 'Account removed') end -end \ No newline at end of file +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 10a4cba8..71fbba5b 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end diff --git a/app/models/commit.rb b/app/models/commit.rb index 8b1cba1d..25d2380e 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -6,4 +6,4 @@ class Commit < ApplicationRecord belongs_to :user validates_presence_of :ref_id, :repository_id, :user_id, :sha -end \ No newline at end of file +end diff --git a/app/models/concerns/encrypted_token.rb b/app/models/concerns/encrypted_token.rb index 6667bee2..41a7a131 100644 --- a/app/models/concerns/encrypted_token.rb +++ b/app/models/concerns/encrypted_token.rb @@ -16,4 +16,4 @@ def decrypted_token(token_value) def encryptor @encryptor ||= ActiveSupport::MessageEncryptor.new(Settings.p4_token_encryption_key) end -end \ No newline at end of file +end diff --git a/app/models/concerns/p4_host_settings.rb b/app/models/concerns/p4_host_settings.rb index 2d414b52..d7044aa4 100644 --- a/app/models/concerns/p4_host_settings.rb +++ b/app/models/concerns/p4_host_settings.rb @@ -19,4 +19,4 @@ def token decrypted_token(tok) end end -end \ No newline at end of file +end diff --git a/app/models/ref.rb b/app/models/ref.rb index 8ee1d929..933dbfec 100644 --- a/app/models/ref.rb +++ b/app/models/ref.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Ref < ApplicationRecord - enum type: [:branch, :tag] + enum type: %i[branch tag] self.inheritance_column = nil diff --git a/app/models/repository_permission.rb b/app/models/repository_permission.rb index 514ae786..7fc67aaa 100644 --- a/app/models/repository_permission.rb +++ b/app/models/repository_permission.rb @@ -4,7 +4,7 @@ class RepositoryPermission < ApplicationRecord belongs_to :repository belongs_to :user - enum permission: [ :read, :write, :admin, :super ] + enum permission: %i[read write admin super] def owner? admin? || super? diff --git a/app/models/server_provider_permission.rb b/app/models/server_provider_permission.rb index d7ac7eb9..a88107df 100644 --- a/app/models/server_provider_permission.rb +++ b/app/models/server_provider_permission.rb @@ -6,5 +6,5 @@ class ServerProviderPermission < ApplicationRecord has_one :setting, class_name: 'ServerProviderUserSetting', foreign_key: :server_provider_user_id, dependent: :delete - enum permission: [:owner, :member] + enum permission: %i[owner member] end diff --git a/app/models/server_provider_user_setting.rb b/app/models/server_provider_user_setting.rb index 93663f9b..129a3c2b 100644 --- a/app/models/server_provider_user_setting.rb +++ b/app/models/server_provider_user_setting.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'P4' class ServerProviderUserSetting < ApplicationRecord diff --git a/app/models/user.rb b/app/models/user.rb index 97a4a2fd..a1a43c55 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class User < ApplicationRecord include Devise::JWT::RevocationStrategies::JTIMatcher @@ -14,14 +16,14 @@ class User < ApplicationRecord otp_secret_encryption_key: Settings.otp_secret_encryption_key has_many :access_grants, - class_name: 'Doorkeeper::AccessGrant', - foreign_key: :resource_owner_id, - dependent: :delete_all + class_name: 'Doorkeeper::AccessGrant', + foreign_key: :resource_owner_id, + dependent: :delete_all has_many :access_tokens, - class_name: 'Doorkeeper::AccessToken', - foreign_key: :resource_owner_id, - dependent: :delete_all + class_name: 'Doorkeeper::AccessToken', + foreign_key: :resource_owner_id, + dependent: :delete_all validate :password_complexity @@ -46,7 +48,7 @@ def repository_permission(repository_id) end def mark_as_deleted - update_columns(email: "deleted_email_#{Kernel.rand(1000000000)}", name: nil, active: false) + update_columns(email: "deleted_email_#{Kernel.rand(1_000_000_000)}", name: nil, active: false) end private @@ -54,6 +56,8 @@ def mark_as_deleted def password_complexity return if password.blank? - errors.add(:password, 'should contain a non-alphabet character (number or special character)') if password =~ /\A[A-Za-z]+\z/ + return unless password =~ /\A[A-Za-z]+\z/ + + errors.add(:password, 'should contain a non-alphabet character (number or special character)') end end diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 8461e0f4..b2e7592c 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -6,5 +6,5 @@ class Webhook < ApplicationRecord validates_presence_of :repository_id, :name, :url scope :active, -> { where(active: true) } - validates :url, url: { schemes: %w(https http) } + validates :url, url: { schemes: %w[https http] } end diff --git a/app/serializers/application_serializer.rb b/app/serializers/application_serializer.rb index 7399b231..615ef8d8 100644 --- a/app/serializers/application_serializer.rb +++ b/app/serializers/application_serializer.rb @@ -9,7 +9,7 @@ def to_h if data[:data].is_a?(Hash) data[:data][:attributes] elsif data[:data].is_a?(Array) - data[:data].map{ |x| x[:attributes] } + data[:data].map { |x| x[:attributes] } elsif data[:data].nil? nil else diff --git a/app/serializers/full_ref_serializer.rb b/app/serializers/full_ref_serializer.rb index af78b550..75ecae58 100644 --- a/app/serializers/full_ref_serializer.rb +++ b/app/serializers/full_ref_serializer.rb @@ -3,5 +3,5 @@ class FullRefSerializer < ApplicationSerializer attributes :id, :name - attribute :type, -> (ref) { ref.type == Ref::BRANCH ? 'branch' : 'tag' } + attribute :type, ->(ref) { ref.type == Ref::BRANCH ? 'branch' : 'tag' } end diff --git a/app/serializers/server_provider_serializer.rb b/app/serializers/server_provider_serializer.rb index 89052fc4..d175b1de 100644 --- a/app/serializers/server_provider_serializer.rb +++ b/app/serializers/server_provider_serializer.rb @@ -2,18 +2,18 @@ class ServerProviderSerializer < ApplicationSerializer PROVIDER_KLASS = { - P4ServerProvider => 'perforce' + P4ServerProvider => 'perforce', }.freeze PERMISSION = { nil => '', 1 => 'Owner', - 2 => 'User' - } + 2 => 'User', + }.freeze attributes :id, :name, :url attribute(:type) { |server| PROVIDER_KLASS[server.type.constantize] } attribute(:username) { |server| server.settings(:p4_host).username } - attribute(:permission) { |server, params| PERMISSION[server.server_provider_permissions&.first&.permission] } + attribute(:permission) { |server, _params| PERMISSION[server.server_provider_permissions&.first&.permission] } end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index a5d1813e..d0979580 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + class UserSerializer < ApplicationSerializer attributes :id, :otp_required_for_login attribute(:name) { |user| user.name || user.email } - attribute(:login) { |user| user.email } - attribute(:emails) { |user| [ user.email ] } + attribute(:login, &:email) + attribute(:emails) { |user| [user.email] } attribute(:servers) { |user| user.server_providers.map(&:id) } - attribute(:uuid) { |user| user.id } + attribute(:uuid, &:id) end diff --git a/app/services/authenticate_user_with_server_provider.rb b/app/services/authenticate_user_with_server_provider.rb index 287d709a..77d96789 100644 --- a/app/services/authenticate_user_with_server_provider.rb +++ b/app/services/authenticate_user_with_server_provider.rb @@ -19,7 +19,7 @@ def call def authenticate_p4 begin ValidateP4Credentials.new(@username, @password, @server_provider.url).call - rescue ValidateP4Credentials::ValidationFailed => e + rescue ValidateP4Credentials::ValidationFailed return false end @@ -28,4 +28,4 @@ def authenticate_p4 setting.username = @username setting.save end -end \ No newline at end of file +end diff --git a/app/services/trigger_webhooks.rb b/app/services/trigger_webhooks.rb index 6f3701dd..326b5992 100644 --- a/app/services/trigger_webhooks.rb +++ b/app/services/trigger_webhooks.rb @@ -12,13 +12,11 @@ def initialize(commit) def call @repository.webhooks.active.each do |webhook| - begin - process_webhook(webhook) - rescue => e - Rails.logger.error "An error happened while processing webhook id=#{webhook.id} name=#{webhook.name}: #{e.message}" - Rails.logger.error "Partial backtrace:" - Rails.logger.error(e.backtrace.first(20).join("\n")) - end + process_webhook(webhook) + rescue StandardError => e + Rails.logger.error "An error happened while processing webhook id=#{webhook.id} name=#{webhook.name}: #{e.message}" + Rails.logger.error 'Partial backtrace:' + Rails.logger.error(e.backtrace.first(20).join("\n")) end end @@ -34,7 +32,7 @@ def process_webhook(webhook) 'Content-Type' => 'application/json' ) - raise WebhookError.new("Request failed: code=#{res.code}, body=#{res.body}") unless res.is_a?(Net::HTTPSuccess) + raise WebhookError, "Request failed: code=#{res.code}, body=#{res.body}" unless res.is_a?(Net::HTTPSuccess) end def webhook_payload(webhook) @@ -46,4 +44,4 @@ def webhook_payload(webhook) } ) end -end \ No newline at end of file +end diff --git a/app/services/update_repository_credentials.rb b/app/services/update_repository_credentials.rb index 49156e80..74ef2d34 100644 --- a/app/services/update_repository_credentials.rb +++ b/app/services/update_repository_credentials.rb @@ -9,7 +9,7 @@ def initialize(entity, username, password) @password = password end - def call + def call # rubocop:disable Metrics/CyclomaticComplexity server_provider = case @entity when Repository then @entity.server_provider when ServerProvider then @entity @@ -30,4 +30,4 @@ def call @entity.save end end -end \ No newline at end of file +end diff --git a/app/services/validate_p4_credentials.rb b/app/services/validate_p4_credentials.rb index 5498d9a6..099aff7b 100644 --- a/app/services/validate_p4_credentials.rb +++ b/app/services/validate_p4_credentials.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'P4' class ValidateP4Credentials @@ -33,16 +34,17 @@ def call nil rescue P4Exception => e - raise ValidationFailed.new(e.message) + raise ValidationFailed, e.message ensure if file begin file.close file.unlink - rescue + rescue StandardError => e + puts e.message.inspect end end ENV.delete('P4TICKETS') end -end \ No newline at end of file +end diff --git a/config.ru b/config.ru index 4a3c09a6..6dc83218 100644 --- a/config.ru +++ b/config.ru @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # This file is used by Rack-based servers to start the application. -require_relative "config/environment" +require_relative 'config/environment' run Rails.application Rails.application.load_server diff --git a/lib/travis/vcs_proxy/repositories/p4.rb b/lib/travis/vcs_proxy/repositories/p4.rb index 6c679da7..0a0d7982 100644 --- a/lib/travis/vcs_proxy/repositories/p4.rb +++ b/lib/travis/vcs_proxy/repositories/p4.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'P4' module Travis @@ -17,7 +18,7 @@ def initialize(repository, url, username, token) def repositories @repositories ||= p4.run_depots.map do |depot| { - name: depot['name'] + name: depot['name'], } end rescue P4Exception => e @@ -29,7 +30,7 @@ def repositories def branches @branches ||= p4.run_streams("//#{@repository.name}/...").map do |stream| { - name: stream['Stream'] + name: stream['Stream'], } end rescue P4Exception => e @@ -44,7 +45,7 @@ def users { email: user['Email'], - name: user['User'] + name: user['User'], } end.compact rescue P4Exception => e @@ -66,7 +67,7 @@ def commits(branch_name) sha: change['change'], user: user, message: change['desc'], - committed_at: Time.at(change['time'].to_i) + committed_at: Time.at(change['time'].to_i), } end end @@ -90,7 +91,7 @@ def file_contents(ref, path) end def commit_info(change_root, username) - matches = change_root.match(/\A\/\/([^\/]+)\/([^\/]+)/) + matches = change_root.match(%r{\A//([^/]+)/([^/]+)}) return if matches.nil? { @@ -132,7 +133,8 @@ def p4 begin file.unlink file.close - rescue + rescue StandardError => e + puts e.message.inspect end end diff --git a/lib/travis/vcs_proxy/sync/repository.rb b/lib/travis/vcs_proxy/sync/repository.rb index 7a263719..bac3ac55 100644 --- a/lib/travis/vcs_proxy/sync/repository.rb +++ b/lib/travis/vcs_proxy/sync/repository.rb @@ -9,7 +9,7 @@ def initialize(repository, user) @user = user end - def sync + def sync # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity username = @repository.settings(:p4_host).username token = @repository.token if username.blank? || token.blank? @@ -22,7 +22,7 @@ def sync return if username.blank? || token.blank? @repository.repo.branches.each do |branch| - branch_name = branch[:name].sub(/\A\/\/#{Regexp.escape(@repository.name)}\//, '') + branch_name = branch[:name].sub(%r{\A//#{Regexp.escape(@repository.name)}/}, '') branch = @repository.branches.find_or_create_by!(name: branch_name) @repository.repo.commits(branch_name).each do |commit| @@ -44,12 +44,13 @@ def sync end perms.each do |email, permission| - next unless users.has_key?(email) + next unless users.key?(email) + user = users[email].first perm = user.repository_permission(@repository.id) if permission == 'none' perm&.delete - return + break end perm ||= user.repository_permissions.build(repository_id: @repository.id) diff --git a/lib/travis/vcs_proxy/sync/server_provider.rb b/lib/travis/vcs_proxy/sync/server_provider.rb index 02ca3ccf..058e51d0 100644 --- a/lib/travis/vcs_proxy/sync/server_provider.rb +++ b/lib/travis/vcs_proxy/sync/server_provider.rb @@ -15,6 +15,7 @@ def sync @server_provider.users.each do |user| next unless permission_setting = user.server_provider_permission(@server_provider.id).setting + sync_repositories(@server_provider.remote_repositories(permission_setting.username, permission_setting.token)) end end diff --git a/spec/controllers/v1/repositories/branches_controller_spec.rb b/spec/controllers/v1/repositories/branches_controller_spec.rb index e89d879c..7640714b 100644 --- a/spec/controllers/v1/repositories/branches_controller_spec.rb +++ b/spec/controllers/v1/repositories/branches_controller_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe V1::Repositories::BranchesController, type: :controller do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/controllers/v1/repositories/commits_controller_spec.rb b/spec/controllers/v1/repositories/commits_controller_spec.rb index f43d7ffd..2c4fe2e6 100644 --- a/spec/controllers/v1/repositories/commits_controller_spec.rb +++ b/spec/controllers/v1/repositories/commits_controller_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe V1::Repositories::CommitsController, type: :controller do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/controllers/v1/repositories/token_controller_spec.rb b/spec/controllers/v1/repositories/token_controller_spec.rb index 6dfda257..41b06dc6 100644 --- a/spec/controllers/v1/repositories/token_controller_spec.rb +++ b/spec/controllers/v1/repositories/token_controller_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe V1::Repositories::TokenController, type: :controller do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/controllers/v1/repositories/webhooks_controller_spec.rb b/spec/controllers/v1/repositories/webhooks_controller_spec.rb index ca82a5c7..41239655 100644 --- a/spec/controllers/v1/repositories/webhooks_controller_spec.rb +++ b/spec/controllers/v1/repositories/webhooks_controller_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe V1::Repositories::WebhooksController, type: :controller do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/controllers/v1/repositories_controller_spec.rb b/spec/controllers/v1/repositories_controller_spec.rb index 69d1ba33..3616f02d 100644 --- a/spec/controllers/v1/repositories_controller_spec.rb +++ b/spec/controllers/v1/repositories_controller_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe V1::RepositoriesController, type: :controller do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/controllers/v1/server_providers_controller_spec.rb b/spec/controllers/v1/server_providers_controller_spec.rb index 5d52e959..4cdcd2e7 100644 --- a/spec/controllers/v1/server_providers_controller_spec.rb +++ b/spec/controllers/v1/server_providers_controller_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe V1::ServerProvidersController, type: :controller do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/controllers/v1/users/confirmations_controller_spec.rb b/spec/controllers/v1/users/confirmations_controller_spec.rb index 7fe9cdcf..12a0a27d 100644 --- a/spec/controllers/v1/users/confirmations_controller_spec.rb +++ b/spec/controllers/v1/users/confirmations_controller_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe V1::Users::ConfirmationsController, type: :controller do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/controllers/v1/users/registrations_controller_spec.rb b/spec/controllers/v1/users/registrations_controller_spec.rb index 501b5fce..621a33b6 100644 --- a/spec/controllers/v1/users/registrations_controller_spec.rb +++ b/spec/controllers/v1/users/registrations_controller_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe V1::Users::RegistrationsController, type: :controller do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/controllers/v1/users/sessions_controller_spec.rb b/spec/controllers/v1/users/sessions_controller_spec.rb index 6ff8c480..61f8dbc2 100644 --- a/spec/controllers/v1/users/sessions_controller_spec.rb +++ b/spec/controllers/v1/users/sessions_controller_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe V1::Users::SessionsController, type: :controller do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/controllers/v1/users/two_factor_auth_controller_spec.rb b/spec/controllers/v1/users/two_factor_auth_controller_spec.rb index 973c6b89..2784a917 100644 --- a/spec/controllers/v1/users/two_factor_auth_controller_spec.rb +++ b/spec/controllers/v1/users/two_factor_auth_controller_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe V1::Users::TwoFactorAuthController, type: :controller do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/controllers/v1/users_controller_spec.rb b/spec/controllers/v1/users_controller_spec.rb index 13a96e1b..eba47276 100644 --- a/spec/controllers/v1/users_controller_spec.rb +++ b/spec/controllers/v1/users_controller_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe V1::UsersController, type: :controller do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/controllers/v1/webhooks_controller_spec.rb b/spec/controllers/v1/webhooks_controller_spec.rb index f4934bb3..d3c52bbd 100644 --- a/spec/controllers/v1/webhooks_controller_spec.rb +++ b/spec/controllers/v1/webhooks_controller_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe V1::WebhooksController, type: :controller do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/models/p4_server_provider_spec.rb b/spec/models/p4_server_provider_spec.rb new file mode 100644 index 00000000..49b33b01 --- /dev/null +++ b/spec/models/p4_server_provider_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe P4ServerProvider, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/p4_service_provider_spec.rb b/spec/models/p4_service_provider_spec.rb deleted file mode 100644 index 1b561863..00000000 --- a/spec/models/p4_service_provider_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe P4ServerProvider, type: :model do - -end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 1c96a561..3516cee7 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe Repository, type: :model do - + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ac11e1ee..4b0bde27 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe User, type: :model do subject { FactoryBot.create(:user) } let(:server_provider) { FactoryBot.create(:server_provider) } - - context 'server_providers' do + + context 'with server_providers' do let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: subject) } describe '#server_provider_permission' do @@ -19,13 +21,13 @@ describe '#set_server_provider_permission' do it 'sets permissions for specified server provider' do subject.set_server_provider_permission(server_provider.id, :member) - + expect(server_provider_permission.reload.permission).to eq('member') end end end - context 'repositories' do + context 'with repositories' do let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: subject) } diff --git a/spec/models/webhook_spec.rb b/spec/models/webhook_spec.rb index e7e79de3..ef9fa681 100644 --- a/spec/models/webhook_spec.rb +++ b/spec/models/webhook_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe Webhook, type: :model do From 23627fc134a5b926288e95869f0b98123f3af74d Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Thu, 9 Sep 2021 14:51:41 +0300 Subject: [PATCH 43/65] Add spec for models --- spec/factories.rb | 12 ++++ spec/models/p4_server_provider_spec.rb | 87 +++++++++++++++++++++++++- spec/models/repository_spec.rb | 41 +++++++++++- spec/models/webhook_spec.rb | 7 --- 4 files changed, 138 insertions(+), 9 deletions(-) delete mode 100644 spec/models/webhook_spec.rb diff --git a/spec/factories.rb b/spec/factories.rb index 1de064fe..7a7e6359 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -19,6 +19,12 @@ permission { :owner } end + factory :p4_server_provider, class: 'P4ServerProvider' do + name { 'TestP4Server' } + url { 'http://test.com/server' } + type { 'P4ServerProvider' } + end + factory :repository do name { 'TestRepo' } url { 'http://test.com/repo' } @@ -31,4 +37,10 @@ association :repository permission { :super } end + + factory :ref do + association :repository + name { 'TestRef' } + type { :branch } + end end diff --git a/spec/models/p4_server_provider_spec.rb b/spec/models/p4_server_provider_spec.rb index 49b33b01..0f390bc0 100644 --- a/spec/models/p4_server_provider_spec.rb +++ b/spec/models/p4_server_provider_spec.rb @@ -3,5 +3,90 @@ require 'rails_helper' RSpec.describe P4ServerProvider, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + subject { FactoryBot.create(:p4_server_provider) } + + describe '#bare_repo' do + context 'when username and password passed' do + let(:username) { 'username' } + let(:password) { '1337PASSWORD' } + + it 'it uses provided user and password to return bare repo' do + expect(Travis::VcsProxy::Repositories::P4).to receive(:new).with(nil, subject.url, username, password).and_call_original + + expect(subject.bare_repo(nil, username, password)).to be_instance_of(Travis::VcsProxy::Repositories::P4) + end + end + + context 'when repository passed' do + let(:username) { 'username' } + let(:token) { 'testtoken' } + let(:repo) { FactoryBot.create(:repository, server_provider: subject, token: token) } + + before do + repo.settings(:p4_host).username = username + end + + it 'it uses repo user and token to return bare repo' do + expect(Travis::VcsProxy::Repositories::P4).to receive(:new).with(repo, subject.url, username, token).and_call_original + + expect(subject.bare_repo(repo)).to be_instance_of(Travis::VcsProxy::Repositories::P4) + end + end + + context 'when nothing is passed' do + let(:username) { 'username' } + let(:token) { 'testtoken' } + + before do + subject.settings(:p4_host).username = username + subject.token = token + end + + it 'uses server user and token' do + expect(Travis::VcsProxy::Repositories::P4).to receive(:new).with(nil, subject.url, username, token).and_call_original + + expect(subject.bare_repo).to be_instance_of(Travis::VcsProxy::Repositories::P4) + end + end + end + + describe '#remote_repositories' do + it 'returns remote repositories' do + expect(subject).to receive(:bare_repo).and_call_original + expect_any_instance_of(Travis::VcsProxy::Repositories::P4).to receive(:repositories) + + subject.remote_repositories + end + end + + describe '#commit_info_from_webhook' do + context 'when payload does not contain change_root and username' do + it 'return nil' do + expect(subject.commit_info_from_webhook({})).to eq(nil) + end + end + + context 'when payload contains change_root and username' do + let(:payload) { { change_root: 'test', username: 'user' } } + + it 'returns commit info from webhook' do + expect(subject).to receive(:bare_repo).and_call_original + expect_any_instance_of(Travis::VcsProxy::Repositories::P4).to receive(:commit_info).with(payload[:change_root], payload[:username]).and_return('test') + + expect(subject.commit_info_from_webhook(payload)).to eq('test') + end + end + end + + describe '#provider_type' do + it 'returns provider_type' do + expect(subject.provider_type).to eq('perforce') + end + end + + describe '#default_branch' do + it 'returns default_branch' do + expect(subject.default_branch).to eq('master') + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 3516cee7..aeba0526 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -3,5 +3,44 @@ require 'rails_helper' RSpec.describe Repository, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + let(:server_provider) { FactoryBot.create(:server_provider) } + + subject { FactoryBot.create(:repository, server_provider: server_provider) } + + let!(:branch_ref) { FactoryBot.create(:ref, name: 'BranchRef', repository: subject, type: :branch) } + let!(:tag_ref) { FactoryBot.create(:ref, name: 'TagRef', repository: subject, type: :tag) } + + describe '#branches' do + it 'returns branch refs' do + expect(subject.branches).to include(branch_ref) + end + end + + + describe '#tags' do + it 'returns tag refs' do + expect(subject.tags).to include(tag_ref) + end + end + + describe '#repo' do + it 'returns bare repo' do + expect(server_provider).to receive(:bare_repo).with(subject) + + subject.repo + end + end + + describe '#file_contents' do + let(:bare_repo) { double } + let(:ref) { 'ref' } + let(:path) { 'path' } + + it 'returns file contents' do + expect(server_provider).to receive(:bare_repo).with(subject).and_return(bare_repo) + expect(bare_repo).to receive(:file_contents).with(ref, path) + + subject.file_contents(ref, path) + end + end end diff --git a/spec/models/webhook_spec.rb b/spec/models/webhook_spec.rb deleted file mode 100644 index ef9fa681..00000000 --- a/spec/models/webhook_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Webhook, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end From 916991ff91699ef9c078079f2fc84ec7164bfcd1 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Thu, 9 Sep 2021 17:15:27 +0300 Subject: [PATCH 44/65] Add some controller specs --- .rubocop.yml | 5 +- .travis.yml | 2 +- .../repositories/branches_controller_spec.rb | 29 +++++- .../repositories/commits_controller_spec.rb | 52 +++++++++- .../v1/repositories/token_controller_spec.rb | 96 ++++++++++++++++++- spec/factories.rb | 9 ++ spec/models/p4_server_provider_spec.rb | 4 +- spec/models/repository_spec.rb | 4 +- 8 files changed, 191 insertions(+), 10 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 25ebeeb1..b2261506 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,7 +11,7 @@ AllCops: - db/* - config/**/* - spec/rails_helper.rb - - spec/spec_helper.rb + - spec/spec_helper.rb TargetRubyVersion: 2.7 Style/Documentation: @@ -80,5 +80,8 @@ RSpec/NamedSubject: RSpec/SubjectStub: Enabled: false +RSpec/LetSetup: + Enabled: false + Lint/UnusedMethodArgument: Enabled: false diff --git a/.travis.yml b/.travis.yml index d9088f91..18112535 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: ruby dist: xenial addons: - postgresql: 12.1 + postgresql: 9.6 rvm: 3.0.1 diff --git a/spec/controllers/v1/repositories/branches_controller_spec.rb b/spec/controllers/v1/repositories/branches_controller_spec.rb index 7640714b..a8090e6f 100644 --- a/spec/controllers/v1/repositories/branches_controller_spec.rb +++ b/spec/controllers/v1/repositories/branches_controller_spec.rb @@ -3,5 +3,32 @@ require 'rails_helper' RSpec.describe V1::Repositories::BranchesController, type: :controller do - pending "add some examples to (or delete) #{__FILE__}" + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + let(:server_provider) { FactoryBot.create(:server_provider) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + let!(:branch_ref) { FactoryBot.create(:ref, name: 'BranchRef', repository: repository, type: :branch) } + + before do + allow_any_instance_of(ApplicationController).to receive(:user_signed_in?).and_return(true) + allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) + end + + describe 'GET index' do + it 'returns branches for specified repository' do + get :index, params: { repository_id: repository.id } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump([{ id: branch_ref.id, name: branch_ref.name, commit: nil }])) + end + end + + describe 'GET show' do + it 'returns specified branch from specified repository' do + get :show, params: { repository_id: repository.id, id: branch_ref.id } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(id: branch_ref.id, name: branch_ref.name, commit: nil)) + end + end end diff --git a/spec/controllers/v1/repositories/commits_controller_spec.rb b/spec/controllers/v1/repositories/commits_controller_spec.rb index 2c4fe2e6..3f4a885d 100644 --- a/spec/controllers/v1/repositories/commits_controller_spec.rb +++ b/spec/controllers/v1/repositories/commits_controller_spec.rb @@ -3,5 +3,55 @@ require 'rails_helper' RSpec.describe V1::Repositories::CommitsController, type: :controller do - pending "add some examples to (or delete) #{__FILE__}" + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + let(:server_provider) { FactoryBot.create(:server_provider) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + let(:branch_ref) { FactoryBot.create(:ref, name: 'BranchRef', repository: repository, type: :branch) } + let!(:commit) { FactoryBot.create(:commit, ref: branch_ref, repository: repository, user: user) } + + before do + allow_any_instance_of(ApplicationController).to receive(:user_signed_in?).and_return(true) + allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) + end + + describe 'GET index' do + it 'returns commits for specified ref and repository' do + get :index, params: { repository_id: repository.id, branch: branch_ref.name } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + [ + { + id: commit.id, + message: commit.message, + sha: commit.sha, + committed_at: commit.committed_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + author: { + name: user.name, + email: user.email, + }, + }, + ] + )) + end + end + + describe 'GET show' do + it 'returns specified branch from specified repository' do + get :show, params: { repository_id: repository.id, branch: branch_ref.name, id: commit.sha } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: commit.id, + message: commit.message, + sha: commit.sha, + committed_at: commit.committed_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + author: { + name: user.name, + email: user.email, + } + )) + end + end end diff --git a/spec/controllers/v1/repositories/token_controller_spec.rb b/spec/controllers/v1/repositories/token_controller_spec.rb index 41b06dc6..d4c778e7 100644 --- a/spec/controllers/v1/repositories/token_controller_spec.rb +++ b/spec/controllers/v1/repositories/token_controller_spec.rb @@ -3,5 +3,99 @@ require 'rails_helper' RSpec.describe V1::Repositories::TokenController, type: :controller do - pending "add some examples to (or delete) #{__FILE__}" + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + let(:server_provider) { FactoryBot.create(:server_provider) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + let(:branch_ref) { FactoryBot.create(:ref, name: 'BranchRef', repository: repository, type: :branch) } + let!(:commit) { FactoryBot.create(:commit, ref: branch_ref, repository: repository, user: user) } + + before do + allow_any_instance_of(ApplicationController).to receive(:user_signed_in?).and_return(true) + allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) + end + + describe 'PATCH update' do + let(:token) { 'token' } + let(:username) { 'username' } + + context 'when username and token are present and user has permission' do + it 'updates the token' do + expect(UpdateRepositoryCredentials).to receive(:new).with(repository, username, token).and_call_original + expect_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(true) + + patch :update, params: { repository_id: repository.id, username: username, token: token } + + expect(response).to be_successful + end + + it 'returns error on unsuccessful update' do + expect(UpdateRepositoryCredentials).to receive(:new).with(repository, username, token).and_call_original + expect_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(false) + + patch :update, params: { repository_id: repository.id, username: username, token: token } + + expect(response.status).to eq(422) + end + end + + context 'when username or token are blank' do + let(:token) { '' } + let(:username) { '' } + + it 'returns bad_request' do + patch :update, params: { repository_id: repository.id, username: username, token: token } + + expect(response).to be_bad_request + end + end + + context 'when user does not have permission' do + before do + repository_permission.permission = :read + repository_permission.save + end + + it 'returns bad_request' do + patch :update, params: { repository_id: repository.id, username: username, token: token } + + expect(response).to be_forbidden + end + end + end + + describe 'DELETE destroy' do + context 'when username and token are present and user has permission' do + it 'destroys the token' do + expect(UpdateRepositoryCredentials).to receive(:new).with(repository, nil, nil).and_call_original + expect_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(true) + + delete :destroy, params: { repository_id: repository.id } + + expect(response).to be_successful + end + + it 'returns error on unsuccessful update' do + expect(UpdateRepositoryCredentials).to receive(:new).with(repository, nil, nil).and_call_original + expect_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(false) + + delete :destroy, params: { repository_id: repository.id } + + expect(response.status).to eq(422) + end + end + + context 'when user does not have permission' do + before do + repository_permission.permission = :read + repository_permission.save + end + + it 'returns bad_request' do + delete :destroy, params: { repository_id: repository.id } + + expect(response).to be_forbidden + end + end + end end diff --git a/spec/factories.rb b/spec/factories.rb index 7a7e6359..5686d5a8 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -43,4 +43,13 @@ name { 'TestRef' } type { :branch } end + + factory :commit do + association :ref + association :repository + association :user + sha { 'COMMIT_SHA' } + message { 'Commit Message' } + committed_at { '2021-09-01 00:00:00' } + end end diff --git a/spec/models/p4_server_provider_spec.rb b/spec/models/p4_server_provider_spec.rb index 0f390bc0..6e8b9fe6 100644 --- a/spec/models/p4_server_provider_spec.rb +++ b/spec/models/p4_server_provider_spec.rb @@ -10,7 +10,7 @@ let(:username) { 'username' } let(:password) { '1337PASSWORD' } - it 'it uses provided user and password to return bare repo' do + it 'uses provided user and password to return bare repo' do expect(Travis::VcsProxy::Repositories::P4).to receive(:new).with(nil, subject.url, username, password).and_call_original expect(subject.bare_repo(nil, username, password)).to be_instance_of(Travis::VcsProxy::Repositories::P4) @@ -26,7 +26,7 @@ repo.settings(:p4_host).username = username end - it 'it uses repo user and token to return bare repo' do + it 'uses repo user and token to return bare repo' do expect(Travis::VcsProxy::Repositories::P4).to receive(:new).with(repo, subject.url, username, token).and_call_original expect(subject.bare_repo(repo)).to be_instance_of(Travis::VcsProxy::Repositories::P4) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index aeba0526..41abb7e8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -3,10 +3,9 @@ require 'rails_helper' RSpec.describe Repository, type: :model do - let(:server_provider) { FactoryBot.create(:server_provider) } - subject { FactoryBot.create(:repository, server_provider: server_provider) } + let(:server_provider) { FactoryBot.create(:server_provider) } let!(:branch_ref) { FactoryBot.create(:ref, name: 'BranchRef', repository: subject, type: :branch) } let!(:tag_ref) { FactoryBot.create(:ref, name: 'TagRef', repository: subject, type: :tag) } @@ -16,7 +15,6 @@ end end - describe '#tags' do it 'returns tag refs' do expect(subject.tags).to include(tag_ref) From a7c522612c99c94c24e745ae209c8295a8a5423f Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Thu, 9 Sep 2021 17:56:08 +0300 Subject: [PATCH 45/65] Update .travis.yml --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 18112535..6c10d524 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: ruby +os: linux dist: xenial addons: @@ -15,12 +16,12 @@ cache: bundler jobs: include: - stage: "rubocop" - scripe: bundle exec rubocop + script: bundle exec rubocop - stage: "rspec" script: bundle exec rspec before_install: - "gem install bundler -v 2.1.4" before_script: - - "RAILS_ENV=test bundle exec rake db:schema:load" + - "RAILS_ENV=test bundle exec rake db:create" services: - - redis-server + - redis From 31418b9d3c6488d36af67885ce89a29703c44da4 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Fri, 10 Sep 2021 17:41:59 +0300 Subject: [PATCH 46/65] Rest of the specs --- .rubocop.yml | 3 + .../v1/server_providers_controller.rb | 2 +- .../v1/users/two_factor_auth_controller.rb | 2 +- app/controllers/v1/webhooks_controller.rb | 2 +- app/serializers/full_ref_serializer.rb | 4 +- app/serializers/server_provider_serializer.rb | 4 +- .../repositories/branches_controller_spec.rb | 3 +- .../repositories/commits_controller_spec.rb | 3 +- .../v1/repositories/token_controller_spec.rb | 3 +- .../repositories/webhooks_controller_spec.rb | 84 +++++- .../v1/repositories_controller_spec.rb | 84 +++++- .../v1/server_providers_controller_spec.rb | 275 +++++++++++++++++- .../v1/users/confirmations_controller_spec.rb | 72 ++++- .../v1/users/registrations_controller_spec.rb | 103 ++++++- .../v1/users/sessions_controller_spec.rb | 150 +++++++++- .../users/two_factor_auth_controller_spec.rb | 79 ++++- spec/controllers/v1/users_controller_spec.rb | 257 +++++++++++++++- .../v1/webhooks_controller_spec.rb | 97 +++++- spec/factories.rb | 10 + 19 files changed, 1214 insertions(+), 23 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index b2261506..96d9df3d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -83,5 +83,8 @@ RSpec/SubjectStub: RSpec/LetSetup: Enabled: false +RSpec/InstanceVariable: + Enabled: false + Lint/UnusedMethodArgument: Enabled: false diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index 1013b0ee..75916a21 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -151,7 +151,7 @@ def set_provider_credentials(provider, errors) success = false begin - success = UpdateRepositoryCredentials.new(provider, params[:server_provider][:username], params[:server_provider][:token]) + success = UpdateRepositoryCredentials.new(provider, params[:server_provider][:username], params[:server_provider][:token]).call rescue UpdateRepositoryCredentials::ValidationFailed errors << 'Cannot authenticate' raise ActiveRecord::Rollback diff --git a/app/controllers/v1/users/two_factor_auth_controller.rb b/app/controllers/v1/users/two_factor_auth_controller.rb index 022336af..bfeef27e 100644 --- a/app/controllers/v1/users/two_factor_auth_controller.rb +++ b/app/controllers/v1/users/two_factor_auth_controller.rb @@ -30,7 +30,7 @@ def enable def codes codes = current_user.generate_otp_backup_codes! - render json: { errors: current_user.errors }, status: :unprocessable_entity unless current_user.save + render(json: { errors: current_user.errors }, status: :unprocessable_entity) && return unless current_user.save render json: { codes: codes } end diff --git a/app/controllers/v1/webhooks_controller.rb b/app/controllers/v1/webhooks_controller.rb index 5ec3dd35..f0319be4 100644 --- a/app/controllers/v1/webhooks_controller.rb +++ b/app/controllers/v1/webhooks_controller.rb @@ -5,7 +5,7 @@ class WebhooksController < ApplicationController def receive # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity head(:unauthorized) && return unless server_provider = ServerProvider.find_by(listener_token: params[:token]) - head :internal_server_error unless commit_info = server_provider.commit_info_from_webhook(params) + head(:internal_server_error) && return unless commit_info = server_provider.commit_info_from_webhook(params) # TODO: Figure out if we should really ignore the hook if there is no user with the given email head(:ok) && return unless user = server_provider.users.find_by(email: commit_info[:email]) diff --git a/app/serializers/full_ref_serializer.rb b/app/serializers/full_ref_serializer.rb index 75ecae58..4bb312e4 100644 --- a/app/serializers/full_ref_serializer.rb +++ b/app/serializers/full_ref_serializer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true class FullRefSerializer < ApplicationSerializer - attributes :id, :name - - attribute :type, ->(ref) { ref.type == Ref::BRANCH ? 'branch' : 'tag' } + attributes :id, :name, :type end diff --git a/app/serializers/server_provider_serializer.rb b/app/serializers/server_provider_serializer.rb index d175b1de..8382e859 100644 --- a/app/serializers/server_provider_serializer.rb +++ b/app/serializers/server_provider_serializer.rb @@ -7,8 +7,8 @@ class ServerProviderSerializer < ApplicationSerializer PERMISSION = { nil => '', - 1 => 'Owner', - 2 => 'User', + 'owner' => 'Owner', + 'member' => 'User', }.freeze attributes :id, :name, :url diff --git a/spec/controllers/v1/repositories/branches_controller_spec.rb b/spec/controllers/v1/repositories/branches_controller_spec.rb index a8090e6f..c60fc97c 100644 --- a/spec/controllers/v1/repositories/branches_controller_spec.rb +++ b/spec/controllers/v1/repositories/branches_controller_spec.rb @@ -10,8 +10,7 @@ let!(:branch_ref) { FactoryBot.create(:ref, name: 'BranchRef', repository: repository, type: :branch) } before do - allow_any_instance_of(ApplicationController).to receive(:user_signed_in?).and_return(true) - allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) + sign_in(user) end describe 'GET index' do diff --git a/spec/controllers/v1/repositories/commits_controller_spec.rb b/spec/controllers/v1/repositories/commits_controller_spec.rb index 3f4a885d..f44c9719 100644 --- a/spec/controllers/v1/repositories/commits_controller_spec.rb +++ b/spec/controllers/v1/repositories/commits_controller_spec.rb @@ -11,8 +11,7 @@ let!(:commit) { FactoryBot.create(:commit, ref: branch_ref, repository: repository, user: user) } before do - allow_any_instance_of(ApplicationController).to receive(:user_signed_in?).and_return(true) - allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) + sign_in(user) end describe 'GET index' do diff --git a/spec/controllers/v1/repositories/token_controller_spec.rb b/spec/controllers/v1/repositories/token_controller_spec.rb index d4c778e7..106d5bb6 100644 --- a/spec/controllers/v1/repositories/token_controller_spec.rb +++ b/spec/controllers/v1/repositories/token_controller_spec.rb @@ -11,8 +11,7 @@ let!(:commit) { FactoryBot.create(:commit, ref: branch_ref, repository: repository, user: user) } before do - allow_any_instance_of(ApplicationController).to receive(:user_signed_in?).and_return(true) - allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) + sign_in(user) end describe 'PATCH update' do diff --git a/spec/controllers/v1/repositories/webhooks_controller_spec.rb b/spec/controllers/v1/repositories/webhooks_controller_spec.rb index 41239655..1e198ace 100644 --- a/spec/controllers/v1/repositories/webhooks_controller_spec.rb +++ b/spec/controllers/v1/repositories/webhooks_controller_spec.rb @@ -3,5 +3,87 @@ require 'rails_helper' RSpec.describe V1::Repositories::WebhooksController, type: :controller do - pending "add some examples to (or delete) #{__FILE__}" + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + let(:server_provider) { FactoryBot.create(:server_provider) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + let!(:webhook) { FactoryBot.create(:webhook, repository: repository) } + + before do + sign_in(user) + end + + describe 'GET index' do + it 'returns webhooks for specified repository' do + get :index, params: { repository_id: repository.id } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + [ + { + id: webhook.id, + name: webhook.name, + url: webhook.url, + active: webhook.active, + insecure_ssl: webhook.insecure_ssl, + created_at: webhook.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + }, + ] + )) + end + end + + describe 'GET show' do + it 'returns webhook for specified repository and id' do + get :show, params: { id: webhook.id, repository_id: repository.id } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: webhook.id, + name: webhook.name, + url: webhook.url, + active: webhook.active, + insecure_ssl: webhook.insecure_ssl, + created_at: webhook.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ') + )) + end + end + + describe 'POST create' do + let(:webhook_params) do + { + name: 'TestHook', + url: 'https://test.url/hook', + active: true, + insecure_ssl: false, + } + end + + it 'creates the webhook and returns its representation' do + post :create, params: { repository_id: repository.id, webhook: webhook_params } + + expect(response).to be_successful + data = JSON.parse(response.body) + expect(data['name']).to eq(webhook_params[:name]) + expect(data['url']).to eq(webhook_params[:url]) + expect(data['active']).to eq(webhook_params[:active]) + expect(data['insecure_ssl']).to eq(webhook_params[:insecure_ssl]) + end + end + + describe 'PATCH update' do + let(:webhook_params) do + { + name: 'TestHookUpdate', + } + end + + it 'updates the webhook and returns its representation' do + patch :update, params: { id: webhook.id, repository_id: repository.id, webhook: webhook_params } + + expect(response).to be_successful + data = JSON.parse(response.body) + expect(data['name']).to eq(webhook_params[:name]) + end + end end diff --git a/spec/controllers/v1/repositories_controller_spec.rb b/spec/controllers/v1/repositories_controller_spec.rb index 3616f02d..404e57b6 100644 --- a/spec/controllers/v1/repositories_controller_spec.rb +++ b/spec/controllers/v1/repositories_controller_spec.rb @@ -3,5 +3,87 @@ require 'rails_helper' RSpec.describe V1::RepositoriesController, type: :controller do - pending "add some examples to (or delete) #{__FILE__}" + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + let(:server_provider) { FactoryBot.create(:p4_server_provider) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + let!(:branch_ref) { FactoryBot.create(:ref, name: 'BranchRef', repository: repository, type: :branch) } + + before do + sign_in(user) + end + + describe 'GET show' do + it 'returns specified repository' do + get :show, params: { id: repository.id } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: repository.id, + name: repository.name, + url: URI.join(Settings.web_url, "servers/#{repository.server_provider_id}"), + token: repository.token, + last_synced_at: repository.last_synced_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + server_provider_id: repository.server_provider_id, + permission: repository_permission.permission, + default_branch: server_provider.default_branch, + owner: { + id: server_provider.id, + }, + slug: "#{server_provider.name}/#{repository.name}", + server_type: server_provider.provider_type + )) + end + end + + describe 'GET refs' do + it 'returns refs from specified repository' do + get :refs, params: { id: repository.id } + + expect(response.body).to eq(JSON.dump([ + { + id: branch_ref.id, + name: branch_ref.name, + type: branch_ref.type, + }, + ])) + end + end + + describe 'GET content' do + context 'when no path is specified' do + it 'returns an error' do + get :content, params: { id: repository.id, ref: branch_ref.id } + + expect(response).to be_bad_request + end + end + + context 'when path is specified' do + let(:path) { 'path' } + let(:contents) { ['', 'contents'] } + + it 'returns the contents' do + expect_any_instance_of(Repository).to receive(:file_contents).and_return(contents) + + get :content, params: { id: repository.id, ref: branch_ref.id, path: path } + + expect(response).to be_successful + expect(response.body).to eq(contents[1]) + end + end + + context 'when path is specified but contents are blank' do + let(:path) { 'path' } + let(:contents) { '' } + + it 'returns the contents' do + expect_any_instance_of(Repository).to receive(:file_contents).and_return(contents) + + get :content, params: { id: repository.id, ref: branch_ref.id, path: path } + + expect(response.status).to eq(422) + end + end + end end diff --git a/spec/controllers/v1/server_providers_controller_spec.rb b/spec/controllers/v1/server_providers_controller_spec.rb index 4cdcd2e7..7d07bef3 100644 --- a/spec/controllers/v1/server_providers_controller_spec.rb +++ b/spec/controllers/v1/server_providers_controller_spec.rb @@ -3,5 +3,278 @@ require 'rails_helper' RSpec.describe V1::ServerProvidersController, type: :controller do - pending "add some examples to (or delete) #{__FILE__}" + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + + before do + sign_in(user) + end + + describe 'POST create' do + let(:params) do + { + server_provider: { + type: 'perforce', + url: 'test.e.corp', + name: 'EcorpServer', + username: 'user', + token: 'token', + }, + } + end + + context 'when no type provided' do + before { params[:server_provider][:type] = '' } + + it 'returns an error' do + post :create, params: params + + expect(response).to be_bad_request + end + end + + context 'when server with provided URL already exists' do + let!(:server_provider) { FactoryBot.create(:server_provider, url: params[:server_provider][:url]) } + + it 'returns an error' do + post :create, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['A server with this URL already exists.'])) + end + end + + context 'when all parameters present but validation fails' do + before do + allow_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(false) + end + + it 'returns an error' do + post :create, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['Cannot save credentials'])) + end + end + + context 'when all parameters present but setting the permission fails' do + before do + allow_any_instance_of(User).to receive(:set_server_provider_permission).and_return(false) + allow_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(true) + end + + it 'returns an error' do + post :create, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['Cannot set permission for user'])) + end + end + + context 'when all parameters present' do + before do + allow_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(true) + end + + it 'creates the server provider' do + expect { post :create, params: params }.to change(ServerProvider, :count).by(1) + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: ServerProvider.last.id, + name: params[:server_provider][:name], + url: params[:server_provider][:url], + type: params[:server_provider][:type], + username: '', + permission: 'Owner' + )) + expect(ServerProvider.last.name).to eq(params[:server_provider][:name]) + end + end + end + + describe 'GET show' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + it 'returns the server provider representation' do + get :show, params: { id: server_provider.id } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: server_provider.id, + name: server_provider.name, + url: server_provider.url, + type: 'perforce', + username: '', + permission: 'Owner' + )) + end + end + + describe 'PATCH update' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let(:params) do + { + id: server_provider.id, + server_provider: { + name: 'TestNameUpdate', + username: 'user', + token: 'token', + }, + } + end + + before do + allow_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(true) + end + + context 'when user has permission' do + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + it 'updates the server provider' do + patch :update, params: params + + expect(response).to be_successful + expect(ServerProvider.last.name).to eq(params[:server_provider][:name]) + end + end + + context 'when user has no permission' do + it 'returns forbidden' do + patch :update, params: params + + expect(response).to be_forbidden + end + end + + context 'when credentials are not validated' do + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + before do + allow_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(false) + end + + it 'returns an error' do + patch :update, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['Cannot save credentials'])) + end + end + end + + describe 'POST authenticate' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let(:params) do + { + id: server_provider.id, + username: 'user', + token: 'token', + } + end + + context 'when no username or token are provided' do + before { params[:username] = '' } + + it 'returns an error' do + post :authenticate, params: params + + expect(response).to be_bad_request + end + end + + context 'when permission exists' do + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + before do + allow_any_instance_of(ValidateP4Credentials).to receive(:call).and_return(true) + end + + it 'updates credentials' do + post :authenticate, params: params + + expect(response).to be_successful + expect(server_provider_permission.setting.username).to eq(params[:username]) + expect(server_provider_permission.setting.token).to eq(params[:token]) + end + end + + context 'when permission does not exist' do + before do + allow_any_instance_of(ValidateP4Credentials).to receive(:call).and_return(true) + end + + it 'creates member permission and updates credentials' do + post :authenticate, params: params + + expect(response).to be_successful + server_provider_permission = ServerProviderPermission.last + expect(server_provider_permission.permission).to eq('member') + expect(server_provider_permission.setting.username).to eq(params[:username]) + expect(server_provider_permission.setting.token).to eq(params[:token]) + end + end + + context 'when validation fails' do + it 'creates member permission and updates credentials' do + post :authenticate, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['Cannot authenticate'])) + end + end + end + + describe 'POST forget' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + it 'removes server provider permission' do + expect { post :forget, params: { id: server_provider.id } }.to change(ServerProviderPermission, :count).by(-1) + end + end + + describe 'POST sync' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + it 'schedules sync for server provider' do + expect(SyncJob).to receive(:perform_later).with(SyncJob::SyncType::SERVER_PROVIDER, server_provider.id, user.id) + + post :sync, params: { id: server_provider.id } + + expect(response).to be_successful + end + end + + describe 'GET by_url' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + it 'returns server provider representation' do + get :by_url, params: { url: server_provider.url } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: server_provider.id, + name: server_provider.name, + url: server_provider.url, + type: 'perforce', + username: '', + permission: 'Owner' + )) + end + end + + describe 'POST add_by_url' do + let(:server_provider) { FactoryBot.create(:server_provider) } + + it 'adds member permission for user' do + post :add_by_url, params: { url: server_provider.url } + + expect(response).to be_successful + server_provider_permission = ServerProviderPermission.last + expect(server_provider_permission.permission).to eq('member') + end + end end diff --git a/spec/controllers/v1/users/confirmations_controller_spec.rb b/spec/controllers/v1/users/confirmations_controller_spec.rb index 12a0a27d..729f86bb 100644 --- a/spec/controllers/v1/users/confirmations_controller_spec.rb +++ b/spec/controllers/v1/users/confirmations_controller_spec.rb @@ -3,5 +3,75 @@ require 'rails_helper' RSpec.describe V1::Users::ConfirmationsController, type: :controller do - pending "add some examples to (or delete) #{__FILE__}" + let(:user) { FactoryBot.create(:user) } + + before { @request.env['devise.mapping'] = Devise.mappings[:user] } + + describe 'GET show' do + let(:confirmation_token) { 'TOKTOKTOKTOKTOK' } + + before do + allow(User).to receive(:confirm_by_token).with(confirmation_token).and_return(user) + end + + context 'when confirmation token is valid' do + it 'confirms the user and redirects to confirmed url' do + get :show, params: { confirmation_token: confirmation_token } + + expect(response).to redirect_to(URI.join(Settings.web_url, 'confirmed').to_s) + end + end + + context 'when confirmation token is invalid' do + before do + user.errors.add('token invalid') + end + + it 'redirects to unconfirmed page' do + get :show, params: { confirmation_token: confirmation_token } + + redirect_uri = URI.join(Settings.web_url, 'unconfirmed') + redirect_uri.query = 'error=expired' + expect(response).to redirect_to(redirect_uri.to_s) + end + end + end + + describe 'POST resend' do + context 'when email is present and user is not confirmed' do + before do + allow_any_instance_of(User).to receive(:confirmed?).and_return(false) + end + + it 'resends the confirmation email' do + expect_any_instance_of(User).to receive(:resend_confirmation_instructions) + + post :resend, params: { email: user.email } + + expect(response).to be_successful + end + end + + context 'when email is present and user is confirmed' do + before do + allow_any_instance_of(User).to receive(:confirmed?).and_return(true) + end + + it 'does not resend the confirmation email' do + expect_any_instance_of(User).not_to receive(:resend_confirmation_instructions) + + post :resend, params: { email: user.email } + + expect(response).to be_successful + end + end + + context 'when email is not present' do + it 'responds with bad request' do + post :resend + + expect(response).to be_bad_request + end + end + end end diff --git a/spec/controllers/v1/users/registrations_controller_spec.rb b/spec/controllers/v1/users/registrations_controller_spec.rb index 621a33b6..a8b3bf84 100644 --- a/spec/controllers/v1/users/registrations_controller_spec.rb +++ b/spec/controllers/v1/users/registrations_controller_spec.rb @@ -3,5 +3,106 @@ require 'rails_helper' RSpec.describe V1::Users::RegistrationsController, type: :controller do - pending "add some examples to (or delete) #{__FILE__}" + before { @request.env['devise.mapping'] = Devise.mappings[:user] } + + describe 'POST create' do + let(:sign_up_params) do + { + email: 'new@user.com', + password: 'Stronk$PASS123', + password_confirmation: 'Stronk$PASS123', + } + end + + context 'when user creation is successful' do + it 'creates user' do + expect do + post :create, params: { user: sign_up_params } + end.to change(User, :count).by(1) + + expect(response).to be_successful + end + end + + context 'when user creation is unsuccessful' do + before do + sign_up_params[:password_confirmation] = 'e' + end + + it 'does not create user and returns unprocessible entity' do + expect do + post :create, params: { user: sign_up_params } + end.not_to change(User, :count) + + expect(response.status).to eq(422) + end + end + end + + describe 'DELETE destroy' do + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + + before do + sign_in(user) + end + + context 'when request has feedback' do + let(:params) do + { + password: 'TestPass#123', + feedback: { + reason: 'Reason', + text: 'Text', + }, + } + end + let(:feedback_mailer) { double } + let(:mail_promise) { double } + + it 'removes user, sends feedback' do + expect(FeedbackMailer).to receive(:with).and_return(feedback_mailer) + expect(feedback_mailer).to receive(:send_feedback).and_return(mail_promise) + expect(mail_promise).to receive(:deliver_now) + + delete :destroy, params: params + + expect(response).to be_successful + expect(user.reload.active).to be_falsey + end + end + + context 'when request does not have feedback' do + let(:params) do + { + password: 'TestPass#123', + } + end + + it 'removes user, sends feedback' do + expect(FeedbackMailer).not_to receive(:with) + + delete :destroy, params: params + + expect(response).to be_successful + expect(user.reload.active).to be_falsey + end + end + + context 'when password is incorrect' do + let(:params) do + { + password: 'Test123', + } + end + + it 'removes user, sends feedback' do + expect(FeedbackMailer).not_to receive(:with) + + delete :destroy, params: params + + expect(response.status).to eq(422) + expect(user.reload.active).to be_truthy + end + end + end end diff --git a/spec/controllers/v1/users/sessions_controller_spec.rb b/spec/controllers/v1/users/sessions_controller_spec.rb index 61f8dbc2..abcf8670 100644 --- a/spec/controllers/v1/users/sessions_controller_spec.rb +++ b/spec/controllers/v1/users/sessions_controller_spec.rb @@ -3,5 +3,153 @@ require 'rails_helper' RSpec.describe V1::Users::SessionsController, type: :controller do - pending "add some examples to (or delete) #{__FILE__}" + let(:user) { FactoryBot.create(:user, otp_secret: User.generate_otp_secret) } + let(:jwt_token) { 'SecretToken' } + + before do + @request.env['devise.mapping'] = Devise.mappings[:user] + allow(controller).to receive(:current_user_jwt_token).and_return(jwt_token) + end + + describe 'POST create' do + context 'when email and password are missing' do + let(:params) do + { + user: { + email: '', + password: '', + }, + } + end + + it 'returns bad request' do + post :create, params: params, format: :json + + expect(response).to be_bad_request + end + end + + context 'when user does not exist' do + let(:params) do + { + user: { + email: 'noemail@e.corp', + password: 'e', + }, + } + end + + it 'returns unauthorized' do + post :create, params: params, format: :json + + expect(response).to be_unauthorized + end + end + + context 'when password is incorrect' do + let(:params) do + { + user: { + email: user.email, + password: 'e', + }, + } + end + + it 'returns an error' do + post :create, params: params, format: :json + + expect(response).to be_unauthorized + expect(response.body).to eq(JSON.dump(error: 'Invalid Email or password.')) + end + end + + context 'when email and password are correct' do + let(:params) do + { + user: { + email: user.email, + password: user.password, + }, + } + end + + it 'returns token and otp enabled' do + post :create, params: params, format: :json + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(token: jwt_token, otp_enabled: false)) + end + end + + context 'when otp is enabled and otp attempt is blank' do + let(:params) do + { + user: { + email: user.email, + password: user.password, + }, + } + end + + before do + user.otp_required_for_login = true + user.save + end + + it 'returns empty token and otp enabled' do + post :create, params: params, format: :json + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(token: '', otp_enabled: true)) + end + end + + context 'when otp is enabled and otp attempt is present and correct' do + let(:params) do + { + user: { + email: user.email, + password: user.password, + otp_attempt: user.current_otp, + }, + } + end + + before do + user.otp_required_for_login = true + user.save + end + + it 'returns token and otp enabled' do + post :create, params: params, format: :json + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(token: jwt_token, otp_enabled: true)) + end + end + + context 'when otp is enabled and otp attempt is present and incorrect' do + let(:params) do + { + user: { + email: user.email, + password: user.password, + otp_attempt: 'random', + }, + } + end + + before do + user.otp_required_for_login = true + user.save + end + + it 'returns unauthorized' do + post :create, params: params, format: :json + + expect(response).to be_unauthorized + end + end + end end diff --git a/spec/controllers/v1/users/two_factor_auth_controller_spec.rb b/spec/controllers/v1/users/two_factor_auth_controller_spec.rb index 2784a917..d3800d73 100644 --- a/spec/controllers/v1/users/two_factor_auth_controller_spec.rb +++ b/spec/controllers/v1/users/two_factor_auth_controller_spec.rb @@ -3,5 +3,82 @@ require 'rails_helper' RSpec.describe V1::Users::TwoFactorAuthController, type: :controller do - pending "add some examples to (or delete) #{__FILE__}" + let(:user) { FactoryBot.create(:user) } + + before { sign_in(user) } + + describe 'GET url' do + let(:otp_secret) { 'secret' } + + it 'generates otp secret and returns the url' do + allow(User).to receive(:generate_otp_secret).and_return(otp_secret) + + get :url + + expect(JSON.parse(response.body)).to eq('url' => "otpauth://totp/Travis%20CI%20VCS%20Proxy:bob%40uncle.com?secret=#{otp_secret}&issuer=Travis%20CI%20VCS%20Proxy") + expect(user.reload.otp_secret).to eq(otp_secret) + end + end + + describe 'POST enable' do + before do + user.otp_secret = User.generate_otp_secret + user.save + end + + context 'when provided otp_attempt is incorrect' do + let(:otp_attempt) { '21345' } + + it 'returns an error' do + post :enable, params: { otp_attempt: otp_attempt } + + expect(response.status).to eq(422) + end + end + + context 'when provided otp_attempt is correct' do + let(:otp_attempt) { user.current_otp } + let(:jwt_token) { 'SecretToken' } + + before { allow(controller).to receive(:current_user_jwt_token).and_return(jwt_token) } + + it 'revokes old token and returns new token' do + expect(User).to receive(:revoke_jwt) + + post :enable, params: { otp_attempt: otp_attempt } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(token: jwt_token, otp_enabled: true)) + end + end + end + + describe 'GET codes' do + let(:codes) { 'codes' } + + context 'when generation is successful' do + before do + allow_any_instance_of(User).to receive(:generate_otp_backup_codes!).and_return(codes) + end + + it 'returns the recoery codes' do + get :codes + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(codes: codes)) + end + end + + context 'when generation is not successful' do + before do + allow_any_instance_of(User).to receive(:save).and_return(false) + end + + it 'returns unprocessible entity' do + get :codes + + expect(response.status).to eq(422) + end + end + end end diff --git a/spec/controllers/v1/users_controller_spec.rb b/spec/controllers/v1/users_controller_spec.rb index eba47276..5d409e42 100644 --- a/spec/controllers/v1/users_controller_spec.rb +++ b/spec/controllers/v1/users_controller_spec.rb @@ -3,5 +3,260 @@ require 'rails_helper' RSpec.describe V1::UsersController, type: :controller do - pending "add some examples to (or delete) #{__FILE__}" + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + + before do + sign_in(user) + end + + describe 'GET show' do + it 'returns the user representation' do + get :show + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: user.id, + otp_required_for_login: user.otp_required_for_login, + name: user.name, + login: user.email, + emails: [user.email], + servers: [], + uuid: user.id + )) + end + end + + describe 'PATCH update_email' do + let(:params) do + { + email: 'email@ecorp.com', + } + end + + context 'when email is provided' do + it 'updates the email' do + patch :update_email, params: params + + expect(response).to be_successful + expect(user.reload.unconfirmed_email).to eq(params[:email]) + end + end + + context 'when email is not provided' do + before { params[:email] = '' } + + it 'does not update the email' do + patch :update_email, params: params + + expect(response).to be_bad_request + expect(user.reload.unconfirmed_email).to eq(nil) + end + end + end + + describe 'PATCH update_password' do + let(:new_password) { 'Stronk$Pass123' } + let(:params) do + { + current_password: user.password, + password: new_password, + password_confirmation: new_password, + } + end + + context 'when valid password is provided' do + it 'changes the password' do + patch :update_password, params: params + + expect(response).to be_successful + end + end + + context 'when one of the parameters is not provided' do + before { params[:password] = '' } + + it 'returns an error' do + patch :update_password, params: params + + expect(response).to be_bad_request + end + end + + context 'when new password does not match confirmation' do + before { params[:password] = 'e' } + + it 'returns an error' do + patch :update_password, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['Password does not match confirmation'])) + end + end + + context 'when new password does not meet requirements' do + before do + params[:password] = 'e' + params[:password_confirmation] = 'e' + end + + it 'returns an error' do + patch :update_password, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: { password: ['is too short (minimum is 6 characters)', 'should contain a non-alphabet character (number or special character)'] })) + end + end + + context 'when current password does not match' do + before { params[:current_password] = 'e' } + + it 'returns an error' do + patch :update_password, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['Invalid current password'])) + end + end + end + + describe 'POST request_password_reset' do + let(:params) do + { + email: user.email, + } + end + + context 'when email is blank' do + before { params[:email] = '' } + + it 'returns no error' do + post :request_password_reset, params: params + + expect(response).to be_successful + end + end + + context 'when email does not belong to any user' do + before { params[:email] = 'no@email.exists' } + + it 'returns no error' do + post :request_password_reset, params: params + + expect(response).to be_successful + end + end + + context 'when email belongs to the user' do + it 'returns no error and requests password reset' do + expect_any_instance_of(User).to receive(:send_reset_password_instructions) + + post :request_password_reset, params: params + + expect(response).to be_successful + end + end + end + + describe 'POST reset_password' do + let(:new_password) { 'Stronk$Pass123' } + let(:params) do + { + reset_password_token: 'token', + password: new_password, + password_confirmation: new_password, + } + end + + context 'when all parameters are present and correct' do + before do + allow(User).to receive(:reset_password_by_token).and_return(user) + end + + it 'resets the password' do + post :reset_password, params: params + + expect(response).to be_successful + end + end + + context 'when reset token is not correct' do + before do + allow(User).to receive(:reset_password_by_token).and_return(user) + user.errors.add(:password, 'Failed') + end + + it 'resets the password' do + post :reset_password, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: { password: ['Failed'] })) + end + end + end + + describe 'GET emails' do + it 'returns users emails' do + get :emails + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(emails: [user.email])) + end + end + + describe 'GET server_providers' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + it 'returns users servers' do + get :server_providers + + expect(response).to be_successful + expect(JSON.parse(response.body)['server_providers']).to eq([ + 'id' => server_provider.id, + 'name' => server_provider.name, + 'url' => server_provider.url, + 'type' => 'perforce', + 'username' => '', + 'permission' => 'Owner', + ]) + end + end + + describe 'GET repositories' do + let(:server_provider) { FactoryBot.create(:p4_server_provider) } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + + it 'returns users repositories' do + get :repositories + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump([{ + id: repository.id, + name: repository.name, + url: URI.join(Settings.web_url, "servers/#{repository.server_provider_id}"), + token: repository.token, + last_synced_at: repository.last_synced_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + server_provider_id: repository.server_provider_id, + permission: repository_permission.permission, + default_branch: server_provider.default_branch, + owner: { + id: server_provider.id, + }, + slug: "#{server_provider.name}/#{repository.name}", + server_type: server_provider.provider_type, + }])) + end + end + + describe 'POST sync' do + it 'schedules sync for server provider' do + expect(SyncJob).to receive(:perform_later).with(SyncJob::SyncType::USER, user.id) + + post :sync + + expect(response).to be_successful + end + end end diff --git a/spec/controllers/v1/webhooks_controller_spec.rb b/spec/controllers/v1/webhooks_controller_spec.rb index d3c52bbd..33f11d57 100644 --- a/spec/controllers/v1/webhooks_controller_spec.rb +++ b/spec/controllers/v1/webhooks_controller_spec.rb @@ -3,5 +3,100 @@ require 'rails_helper' RSpec.describe V1::WebhooksController, type: :controller do - pending "add some examples to (or delete) #{__FILE__}" + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + let(:token) { 'token' } + let(:server_provider) { FactoryBot.create(:p4_server_provider, listener_token: 'token') } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + + before do + sign_in(user) + allow_any_instance_of(P4ServerProvider).to receive(:commit_info_from_webhook).and_return(commit_info) + end + + describe 'POST receive' do + let(:params) do + { + token: token, + change_root: 'root', + username: user.email, + } + end + let(:commit_info) do + { + email: user.email, + repository_name: repository.name, + sha: 'sha', + ref: 'ref', + } + end + + context 'when all parameters are provided and correct' do + let(:trigger_webhooks) { double } + + it 'triggers webhooks' do + expect(TriggerWebhooks).to receive(:new).and_return(trigger_webhooks) + expect(trigger_webhooks).to receive(:call) + + post :receive, params: params + + expect(response).to be_successful + end + end + + context 'when server is not found' do + let(:token) { 'notoken' } + + it 'returns an error' do + expect(TriggerWebhooks).not_to receive(:new) + + post :receive, params: params + + expect(response).to be_unauthorized + end + end + + context 'when there is no commit info' do + before do + allow_any_instance_of(P4ServerProvider).to receive(:commit_info_from_webhook).and_return(nil) + end + + it 'returns an error' do + expect(TriggerWebhooks).not_to receive(:new) + + post :receive, params: params + + expect(response.status).to eq(500) + end + end + + context 'when there is no user' do + before do + commit_info[:email] = 'no@email.com' + end + + it 'does not trigger webhooks' do + expect(TriggerWebhooks).not_to receive(:new) + + post :receive, params: params + + expect(response).to be_successful + end + end + + context 'when there is no repository' do + before do + commit_info[:repository_name] = 'NoREPO' + end + + it 'does not trigger webhooks' do + expect(TriggerWebhooks).not_to receive(:new) + + post :receive, params: params + + expect(response).to be_successful + end + end + end end diff --git a/spec/factories.rb b/spec/factories.rb index 5686d5a8..730d5620 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -5,6 +5,7 @@ name { 'Bob Uncle' } email { 'bob@uncle.com' } password { 'TestPass#123' } + confirmed_at { Date.today } end factory :server_provider do @@ -52,4 +53,13 @@ message { 'Commit Message' } committed_at { '2021-09-01 00:00:00' } end + + factory :webhook do + association :repository + name { 'RepoWebHook' } + url { 'https://webhook.repo/' } + active { true } + insecure_ssl { false } + created_at { '2021-09-01 00:00:00' } + end end From 930098641bb22f7ef48986ad61f2eb9e460f0df4 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Mon, 13 Sep 2021 11:42:19 +0300 Subject: [PATCH 47/65] Fix server provider serializer --- app/serializers/server_provider_serializer.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/serializers/server_provider_serializer.rb b/app/serializers/server_provider_serializer.rb index 8382e859..d33d33b8 100644 --- a/app/serializers/server_provider_serializer.rb +++ b/app/serializers/server_provider_serializer.rb @@ -14,6 +14,10 @@ class ServerProviderSerializer < ApplicationSerializer attributes :id, :name, :url attribute(:type) { |server| PROVIDER_KLASS[server.type.constantize] } - attribute(:username) { |server| server.settings(:p4_host).username } + attribute(:username) do |server| + permission = server.server_provider_permissions&.first + setting = permission&.setting || permission&.build_setting + setting&.username || '' + end attribute(:permission) { |server, _params| PERMISSION[server.server_provider_permissions&.first&.permission] } end From 9e67905e213bf18b68450143d79b0db5e528c653 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Mon, 13 Sep 2021 14:26:32 +0300 Subject: [PATCH 48/65] Revert "Fix server provider serializer" This reverts commit 930098641bb22f7ef48986ad61f2eb9e460f0df4. --- app/serializers/server_provider_serializer.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/serializers/server_provider_serializer.rb b/app/serializers/server_provider_serializer.rb index d33d33b8..8382e859 100644 --- a/app/serializers/server_provider_serializer.rb +++ b/app/serializers/server_provider_serializer.rb @@ -14,10 +14,6 @@ class ServerProviderSerializer < ApplicationSerializer attributes :id, :name, :url attribute(:type) { |server| PROVIDER_KLASS[server.type.constantize] } - attribute(:username) do |server| - permission = server.server_provider_permissions&.first - setting = permission&.setting || permission&.build_setting - setting&.username || '' - end + attribute(:username) { |server| server.settings(:p4_host).username } attribute(:permission) { |server, _params| PERMISSION[server.server_provider_permissions&.first&.permission] } end From ade7b7c44ad40c7bc9d03ca3dd82c455ee93547f Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Mon, 13 Sep 2021 15:08:08 +0300 Subject: [PATCH 49/65] Pass credentials to repo --- app/models/repository.rb | 4 ++-- lib/travis/vcs_proxy/sync/repository.rb | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index d5b459e4..07ce19f0 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -22,8 +22,8 @@ def tags refs.tag end - def repo - server_provider.bare_repo(self) + def repo(username = nil, token = nil) + server_provider.bare_repo(self, username, token) end def file_contents(ref, path) diff --git a/lib/travis/vcs_proxy/sync/repository.rb b/lib/travis/vcs_proxy/sync/repository.rb index bac3ac55..38964bb1 100644 --- a/lib/travis/vcs_proxy/sync/repository.rb +++ b/lib/travis/vcs_proxy/sync/repository.rb @@ -21,18 +21,20 @@ def sync # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComple end return if username.blank? || token.blank? - @repository.repo.branches.each do |branch| + repo = @repository.repo(username, token) + + repo.branches.each do |branch| branch_name = branch[:name].sub(%r{\A//#{Regexp.escape(@repository.name)}/}, '') branch = @repository.branches.find_or_create_by!(name: branch_name) - @repository.repo.commits(branch_name).each do |commit| + repo.commits(branch_name).each do |commit| next if branch.commits.where(sha: commit[:sha]).exists? branch.commits.create!(commit.merge(repository_id: @repository.id)) end end - perms = @repository.repo.permissions + perms = repo.permissions if perms.present? repo_emails = perms.keys users = ::User.where(email: repo_emails).group_by(&:email) From 55a331cab18434a8d4a94bbc632724cc2d59bca6 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Mon, 13 Sep 2021 15:10:47 +0300 Subject: [PATCH 50/65] Rubocop --- lib/travis/vcs_proxy/sync/repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/travis/vcs_proxy/sync/repository.rb b/lib/travis/vcs_proxy/sync/repository.rb index 38964bb1..34bbcfb0 100644 --- a/lib/travis/vcs_proxy/sync/repository.rb +++ b/lib/travis/vcs_proxy/sync/repository.rb @@ -9,7 +9,7 @@ def initialize(repository, user) @user = user end - def sync # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def sync # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength username = @repository.settings(:p4_host).username token = @repository.token if username.blank? || token.blank? From 3640c35225f1351921a906032222a29b557a8a4f Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Mon, 13 Sep 2021 15:32:17 +0300 Subject: [PATCH 51/65] Fix spec --- spec/models/repository_spec.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 41abb7e8..70f94cf3 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -23,10 +23,16 @@ describe '#repo' do it 'returns bare repo' do - expect(server_provider).to receive(:bare_repo).with(subject) + expect(server_provider).to receive(:bare_repo).with(subject, nil, nil) subject.repo end + + it 'authorizes and returns bare repo' do + expect(server_provider).to receive(:bare_repo).with(subject, 'test', 'token') + + subject.repo('test', 'token') + end end describe '#file_contents' do From c12b0aa46360ef01fe9f5b25f8f3123849139609 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Mon, 13 Sep 2021 15:36:02 +0300 Subject: [PATCH 52/65] Fix spec --- spec/models/repository_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 70f94cf3..9d16bf1b 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -41,7 +41,7 @@ let(:path) { 'path' } it 'returns file contents' do - expect(server_provider).to receive(:bare_repo).with(subject).and_return(bare_repo) + expect(server_provider).to receive(:bare_repo).with(subject, nil, nil).and_return(bare_repo) expect(bare_repo).to receive(:file_contents).with(ref, path) subject.file_contents(ref, path) From 5568655e092310bff4369c9bc4ff7d25fbefc31a Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Mon, 13 Sep 2021 16:14:44 +0300 Subject: [PATCH 53/65] Use p4 password --- app/services/validate_p4_credentials.rb | 8 ++------ lib/travis/vcs_proxy/repositories/p4.rb | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/app/services/validate_p4_credentials.rb b/app/services/validate_p4_credentials.rb index 099aff7b..85aa13c2 100644 --- a/app/services/validate_p4_credentials.rb +++ b/app/services/validate_p4_credentials.rb @@ -18,16 +18,12 @@ def initialize(username, token, url) end def call - file = Tempfile.new('p4ticket') - file.write(@token) - file.close - - ENV['P4TICKETS'] = file.path - p4 = P4.new p4.charset = 'utf8' p4.port = @url p4.user = @username + p4.password = @token + p4.ticket_file = '/dev/null' p4.connect p4.run_trust('-y') p4.run_protects diff --git a/lib/travis/vcs_proxy/repositories/p4.rb b/lib/travis/vcs_proxy/repositories/p4.rb index 0a0d7982..7c6d6170 100644 --- a/lib/travis/vcs_proxy/repositories/p4.rb +++ b/lib/travis/vcs_proxy/repositories/p4.rb @@ -110,16 +110,12 @@ def commit_info(change_root, username) def p4 return @p4 if defined?(@p4) - file = Tempfile.new('p4ticket') - file.write(@token) - file.close - - ENV['P4TICKETS'] = file.path - @p4 = ::P4.new @p4.charset = 'utf8' @p4.port = @url @p4.user = @username + @p4.password = @token + @p4.ticket_file = '/dev/null' @p4.connect @p4.run_trust('-y') @p4.run_protects From 3fbdbd88e4da6d34008a1323eb09c063085fc906 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Mon, 13 Sep 2021 16:18:40 +0300 Subject: [PATCH 54/65] Fix --- app/services/validate_p4_credentials.rb | 11 ----------- lib/travis/vcs_proxy/repositories/p4.rb | 11 ----------- 2 files changed, 22 deletions(-) diff --git a/app/services/validate_p4_credentials.rb b/app/services/validate_p4_credentials.rb index 85aa13c2..b9bfc2e0 100644 --- a/app/services/validate_p4_credentials.rb +++ b/app/services/validate_p4_credentials.rb @@ -31,16 +31,5 @@ def call nil rescue P4Exception => e raise ValidationFailed, e.message - ensure - if file - begin - file.close - file.unlink - rescue StandardError => e - puts e.message.inspect - end - end - - ENV.delete('P4TICKETS') end end diff --git a/lib/travis/vcs_proxy/repositories/p4.rb b/lib/travis/vcs_proxy/repositories/p4.rb index 7c6d6170..2ed64833 100644 --- a/lib/travis/vcs_proxy/repositories/p4.rb +++ b/lib/travis/vcs_proxy/repositories/p4.rb @@ -124,17 +124,6 @@ def p4 rescue P4Exception => e puts e.message.inspect raise - ensure - if file - begin - file.unlink - file.close - rescue StandardError => e - puts e.message.inspect - end - end - - ENV.delete('P4TICKETS') end end end From 0bb5ccf767005acb21b760751fbb47a85b1b8642 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Mon, 13 Sep 2021 16:46:56 +0300 Subject: [PATCH 55/65] Fix exception and remove logging --- lib/travis/vcs_proxy/repositories/p4.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/travis/vcs_proxy/repositories/p4.rb b/lib/travis/vcs_proxy/repositories/p4.rb index 2ed64833..5c324120 100644 --- a/lib/travis/vcs_proxy/repositories/p4.rb +++ b/lib/travis/vcs_proxy/repositories/p4.rb @@ -59,7 +59,6 @@ def commits(branch_name) memo[user[:name]] = user[:email] end p4.run_changes('-l', "//#{@repository.name}/#{branch_name}/...").map do |change| - puts change.inspect next unless email = user_map[change['user']] next unless user = User.find_by(email: email) @@ -69,7 +68,7 @@ def commits(branch_name) message: change['desc'], committed_at: Time.at(change['time'].to_i), } - end + end.compact end def permissions From 051d32abd9fd9418f3e05bc71fe2cd4a7ec62c9e Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Mon, 13 Sep 2021 17:06:46 +0300 Subject: [PATCH 56/65] Do not throw an exception when not enough perms for checking permissions --- lib/travis/vcs_proxy/repositories/p4.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/travis/vcs_proxy/repositories/p4.rb b/lib/travis/vcs_proxy/repositories/p4.rb index 5c324120..6ea7ff09 100644 --- a/lib/travis/vcs_proxy/repositories/p4.rb +++ b/lib/travis/vcs_proxy/repositories/p4.rb @@ -73,12 +73,12 @@ def commits(branch_name) def permissions @permissions ||= users.each_with_object({}) do |user, memo| - memo[user[:email]] = p4.run_protects('-u', user[:name], '-M', "//#{@repository.name}/...").first['permMax'] + begin + memo[user[:email]] = p4.run_protects('-u', user[:name], '-M', "//#{@repository.name}/...").first['permMax'] + rescue P4Exception => e + puts e.message.inspect + end end - rescue P4Exception => e - puts e.message.inspect - - {} end def file_contents(ref, path) From f7eb0742d9f588c76fe95a49e1611832c51d2972 Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Mon, 13 Sep 2021 18:11:40 +0300 Subject: [PATCH 57/65] Use server provider token if no user and repo token is available --- lib/travis/vcs_proxy/sync/repository.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/travis/vcs_proxy/sync/repository.rb b/lib/travis/vcs_proxy/sync/repository.rb index 34bbcfb0..010aa8d7 100644 --- a/lib/travis/vcs_proxy/sync/repository.rb +++ b/lib/travis/vcs_proxy/sync/repository.rb @@ -13,12 +13,17 @@ def sync # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComple username = @repository.settings(:p4_host).username token = @repository.token if username.blank? || token.blank? - return unless server_provider_permission = @user.server_provider_permission(@repository.server_provider_id) - return unless server_provider_user_setting = server_provider_permission.setting + if server_provider_user_setting = @user.server_provider_permission(@repository.server_provider_id)&.setting + username = server_provider_user_setting.username + token = server_provider_user_setting.token + end + end - username = server_provider_user_setting.username - token = server_provider_user_setting.token + if username.blank? || token.blank? + username = @repository.server_provider.settings(:p4_host).username + token = @repository.server_provider.token end + return if username.blank? || token.blank? repo = @repository.repo(username, token) From f8ac8d0a8b0f21b431db12922f577a76269d523d Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Wed, 15 Sep 2021 15:35:31 +0300 Subject: [PATCH 58/65] Fix serializers --- app/serializers/repository_serializer.rb | 2 +- app/serializers/server_provider_serializer.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/serializers/repository_serializer.rb b/app/serializers/repository_serializer.rb index 556e0189..f86234f9 100644 --- a/app/serializers/repository_serializer.rb +++ b/app/serializers/repository_serializer.rb @@ -3,7 +3,7 @@ class RepositorySerializer < ApplicationSerializer attributes :id, :name, :url, :token, :last_synced_at, :server_provider_id - attributes(:permission) { |repo| repo.permissions&.first&.permission } + attributes(:permission) { |repo, params| params[:current_user].repository_permission(repo.id)&.permission } attributes(:default_branch) { |repo| repo.server_provider.default_branch } attributes(:url) { |repo| URI.join(Settings.web_url, "servers/#{repo.server_provider_id}") } attributes(:owner) do |repo| diff --git a/app/serializers/server_provider_serializer.rb b/app/serializers/server_provider_serializer.rb index 8382e859..66126009 100644 --- a/app/serializers/server_provider_serializer.rb +++ b/app/serializers/server_provider_serializer.rb @@ -15,5 +15,5 @@ class ServerProviderSerializer < ApplicationSerializer attribute(:type) { |server| PROVIDER_KLASS[server.type.constantize] } attribute(:username) { |server| server.settings(:p4_host).username } - attribute(:permission) { |server, _params| PERMISSION[server.server_provider_permissions&.first&.permission] } + attribute(:permission) { |server, params| PERMISSION[params[:current_user].server_provider_permission(server.id)&.permission] } end From 331f2e1f31d1254aea15507c2626e85337af7a1f Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Tue, 21 Sep 2021 19:04:05 +0300 Subject: [PATCH 59/65] Add Sentry --- Gemfile | 3 +++ Gemfile.lock | 37 +++++++++++++++++++++++++++++++++++ config/initializers/sentry.rb | 9 +++++++++ config/settings.yml | 2 ++ 4 files changed, 51 insertions(+) create mode 100644 config/initializers/sentry.rb diff --git a/Gemfile b/Gemfile index c9f82e24..85eef2e4 100644 --- a/Gemfile +++ b/Gemfile @@ -22,6 +22,9 @@ gem 'rails', '~> 6.1.3', '>= 6.1.3.2' gem 'redis' gem 'sidekiq' gem 'validate_url' +gem 'sentry-ruby' +gem 'sentry-rails' +gem 'sentry-sidekiq' group :development, :test do gem 'brakeman' diff --git a/Gemfile.lock b/Gemfile.lock index 1fd06a0c..95895bb2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -140,6 +140,25 @@ GEM erubi (1.10.0) factory_bot (6.2.0) activesupport (>= 5.0.0) + faraday (1.8.0) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0.1) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.1) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + multipart-post (>= 1.2, < 3) + 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-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) ffi (1.15.1) globalid (0.4.2) activesupport (>= 4.2.0) @@ -177,6 +196,7 @@ GEM mini_portile2 (2.5.3) minitest (5.14.4) msgpack (1.4.2) + multipart-post (2.1.1) nio4r (2.5.7) nokogiri (1.11.7) mini_portile2 (~> 2.5.0) @@ -263,6 +283,20 @@ GEM rubocop-rspec (1.41.0) rubocop (>= 0.68.1) ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) + sentry-rails (4.7.2) + railties (>= 5.0) + sentry-ruby-core (~> 4.7.0) + sentry-ruby (4.7.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + faraday (>= 1.0) + sentry-ruby-core (= 4.7.2) + sentry-ruby-core (4.7.2) + concurrent-ruby + faraday + sentry-sidekiq (4.7.2) + sentry-ruby-core (~> 4.7.0) + sidekiq (>= 3.0) sidekiq (6.2.1) connection_pool (>= 2.2.2) rack (~> 2.0) @@ -321,6 +355,9 @@ DEPENDENCIES rspec-rails rubocop (~> 0.75.1) rubocop-rspec + sentry-rails + sentry-ruby + sentry-sidekiq sidekiq validate_url diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb new file mode 100644 index 00000000..f7307fe6 --- /dev/null +++ b/config/initializers/sentry.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +if Settings.sentry.dsn.present? + Sentry.init do |config| + config.dsn = Settings.sentry.dsn + config.breadcrumbs_logger = [:active_support_logger, :http_logger] + end +end + diff --git a/config/settings.yml b/config/settings.yml index a58eddd9..125dc0ca 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -6,3 +6,5 @@ web_url: https://travis-vcs-proxy.travis-ci.org api_url: https://travis-vcs-proxy-api.travis-ci.org feedback_mail: 'cancellations@travis-ci.com' mail_from: test@example.com +sentry: + dsn: fill-me From fe5f29c6895477f024ed30799a7f85a2ea52e3b7 Mon Sep 17 00:00:00 2001 From: Andrii Mysko Date: Wed, 29 Sep 2021 14:44:11 +0300 Subject: [PATCH 60/65] Fix rubocop --- Gemfile | 4 ++-- lib/travis/vcs_proxy/repositories/p4.rb | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 85eef2e4..97eb2f86 100644 --- a/Gemfile +++ b/Gemfile @@ -21,10 +21,10 @@ gem 'rack-cors' gem 'rails', '~> 6.1.3', '>= 6.1.3.2' gem 'redis' gem 'sidekiq' -gem 'validate_url' -gem 'sentry-ruby' gem 'sentry-rails' +gem 'sentry-ruby' gem 'sentry-sidekiq' +gem 'validate_url' group :development, :test do gem 'brakeman' diff --git a/lib/travis/vcs_proxy/repositories/p4.rb b/lib/travis/vcs_proxy/repositories/p4.rb index 6ea7ff09..8f1eb662 100644 --- a/lib/travis/vcs_proxy/repositories/p4.rb +++ b/lib/travis/vcs_proxy/repositories/p4.rb @@ -73,11 +73,9 @@ def commits(branch_name) def permissions @permissions ||= users.each_with_object({}) do |user, memo| - begin - memo[user[:email]] = p4.run_protects('-u', user[:name], '-M', "//#{@repository.name}/...").first['permMax'] - rescue P4Exception => e - puts e.message.inspect - end + memo[user[:email]] = p4.run_protects('-u', user[:name], '-M', "//#{@repository.name}/...").first['permMax'] + rescue P4Exception => e + puts e.message.inspect end end From f596464c2fc4246dac3007a9620c7fab5dac6e89 Mon Sep 17 00:00:00 2001 From: Andrii Mysko Date: Wed, 29 Sep 2021 14:47:56 +0300 Subject: [PATCH 61/65] Fix rubocop --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 97eb2f86..4424c033 100644 --- a/Gemfile +++ b/Gemfile @@ -20,10 +20,10 @@ gem 'puma', '~> 5.0' gem 'rack-cors' gem 'rails', '~> 6.1.3', '>= 6.1.3.2' gem 'redis' -gem 'sidekiq' gem 'sentry-rails' gem 'sentry-ruby' gem 'sentry-sidekiq' +gem 'sidekiq' gem 'validate_url' group :development, :test do From 89882b6958c1ac224055a5eb3b851831e1b75c1c Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Fri, 1 Oct 2021 11:24:35 +0300 Subject: [PATCH 62/65] webhook creation/trigger fix, file contents fix/wa --- app/controllers/v1/webhooks_controller.rb | 2 +- app/services/trigger_webhooks.rb | 37 +++++++++++++++++++---- lib/travis/vcs_proxy/repositories/p4.rb | 2 +- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/app/controllers/v1/webhooks_controller.rb b/app/controllers/v1/webhooks_controller.rb index f0319be4..1f786854 100644 --- a/app/controllers/v1/webhooks_controller.rb +++ b/app/controllers/v1/webhooks_controller.rb @@ -15,7 +15,7 @@ def receive # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedCom ref = repository.refs.branch.find_by(name: commit_info[:ref]) || repository.refs.branch.create(name: commit_info[:ref]) head(:unprocessable_entity) && return unless ref - commit = ref.commits.find_by(sha: params[:sha]) || ref.commits.create(sha: params[:sha], repository: repository, user: user) + commit = ref.commits.find_by(sha: params[:sha]) || ref.commits.create(sha: params[:sha], repository: repository, user: user, committed_at: Time.now) head(:unprocessable_entity) && return unless commit TriggerWebhooks.new(commit).call diff --git a/app/services/trigger_webhooks.rb b/app/services/trigger_webhooks.rb index 326b5992..dba10b53 100644 --- a/app/services/trigger_webhooks.rb +++ b/app/services/trigger_webhooks.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +require 'securerandom' class TriggerWebhooks class WebhookError < StandardError; end @@ -7,6 +8,7 @@ def initialize(commit) @commit = commit @ref = commit.ref @repository = @ref.repository + @user = commit.user @server_provider = @repository.server_provider end @@ -29,7 +31,9 @@ def process_webhook(webhook) res = Net::HTTP.post( uri, webhook_payload(webhook), - 'Content-Type' => 'application/json' + 'Content-Type' => 'application/json', + 'X-Travisproxy-Event-Id' => SecureRandom.uuid, + 'X-Request-Id' => SecureRandom.uuid ) raise WebhookError, "Request failed: code=#{res.code}, body=#{res.body}" unless res.is_a?(Net::HTTPSuccess) @@ -37,11 +41,32 @@ def process_webhook(webhook) def webhook_payload(webhook) JSON.dump( - commit: { - sha: @commit.sha, - message: @commit.message, - committed_at: @commit.committed_at, - } + branch_name: 'main', + sender_id: @commit.user_id.to_s, + new_revision: @commit.sha, + sender_login: @user.email, + server_type: 'perforce', + owner_vcs_id: @server_provider.id.to_s, + sender_vcs_id: @commit.user_id.to_s, + repository: { + id: @repository.id.to_s, + name: @repository.name, + full_name: @repository.name, + slug: @repository.url, + is_private: true, + + }, + commits: [ + { + id: @commit.id.to_s, + sha: @commit.sha, + revision: @commit.sha, + message: @commit.message || '', + committed_at: @commit.committed_at, + commiter_name: @user.name || '', + commiter_email: @user.email || '', + }, + ] ) end end diff --git a/lib/travis/vcs_proxy/repositories/p4.rb b/lib/travis/vcs_proxy/repositories/p4.rb index 8f1eb662..35480723 100644 --- a/lib/travis/vcs_proxy/repositories/p4.rb +++ b/lib/travis/vcs_proxy/repositories/p4.rb @@ -80,7 +80,7 @@ def permissions end def file_contents(ref, path) - p4.run_print("//#{@repository.name}/#{ref}/#{path}") + p4.run_print("//#{@repository.name}/#{path}") rescue P4Exception => e puts e.message.inspect From 62ec4d39d0e61ad45020459f5d6cdd49af209216 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Fri, 1 Oct 2021 11:25:20 +0300 Subject: [PATCH 63/65] added token get api --- app/controllers/v1/repositories/token_controller.rb | 7 +++++++ config/routes.rb | 1 + 2 files changed, 8 insertions(+) diff --git a/app/controllers/v1/repositories/token_controller.rb b/app/controllers/v1/repositories/token_controller.rb index 35a9521b..13543cac 100644 --- a/app/controllers/v1/repositories/token_controller.rb +++ b/app/controllers/v1/repositories/token_controller.rb @@ -6,6 +6,13 @@ class TokenController < ApplicationController before_action :require_authentication before_action :set_repository + def get + permission = current_user.repository_permission(@repository.id) + head(:forbidden) && return if permission.blank? || (!permission.owner? && !permission.admin?) + + render json: { token: @repository.decrypted_token(@repository.server_provider.settings(:p4_host).token) } + end + def update # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity head(:bad_request) && return if params[:username].blank? || params[:token].blank? diff --git a/config/routes.rb b/config/routes.rb index 87cfe070..fba373ac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -71,6 +71,7 @@ resources :webhooks, controller: 'repositories/webhooks', only: [:index, :show, :create, :update] resources :token, controller: 'repositories/token', only: [] do collection do + get :get patch :update delete :destroy end From 259606c80e4650262f393736adf55cc68959f590 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Thu, 14 Oct 2021 19:59:31 +0300 Subject: [PATCH 64/65] saving settings on sp.update --- app/controllers/v1/server_providers_controller.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb index 75916a21..0afde728 100644 --- a/app/controllers/v1/server_providers_controller.rb +++ b/app/controllers/v1/server_providers_controller.rb @@ -59,6 +59,12 @@ def update # rubocop:disable Metrics/CyclomaticComplexity end set_provider_credentials(@server_provider, errors) + + setting = permission.setting || permission.build_setting + setting.token = params[:server_provider][:token] + setting.username = params[:server_provider][:username] + setting.save + end head(:ok) && return if errors.blank? From 340bc94b18ce5b272257326d75fa664258cdbecd Mon Sep 17 00:00:00 2001 From: Stanislav Kolotinskiy Date: Fri, 12 Nov 2021 10:07:48 +0200 Subject: [PATCH 65/65] Set a valid email when marking user as deleted --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index a1a43c55..fa91f081 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -48,7 +48,7 @@ def repository_permission(repository_id) end def mark_as_deleted - update_columns(email: "deleted_email_#{Kernel.rand(1_000_000_000)}", name: nil, active: false) + update_columns(email: "deleted_email_#{Kernel.rand(1_000_000_000)}@example.com", name: nil, active: false) end private