diff --git a/.env.example b/.env.example index f11568174..882dfe72f 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,6 @@ ALLOWED_ORIGINS=localhost:3002,localhost:3000 POSTGRES_HOST=changeme POSTGRES_USER=changeme POSTGRES_PASSWORD=changeme + +HYDRA_ADMIN_URL=http://docker.for.mac.localhost:9001 +HYDRA_SECRET= diff --git a/Gemfile b/Gemfile index abd0b4b80..cb2a7d483 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '3.0.3' gem 'bootsnap', require: false +gem 'faraday' gem 'importmap-rails' gem 'jbuilder' gem 'pg', '~> 1.1' @@ -32,4 +33,5 @@ end group :test do gem 'shoulda-matchers', '~> 5.0' + gem 'webmock' end diff --git a/Gemfile.lock b/Gemfile.lock index 246366e39..2ac248da0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,6 +60,8 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) ast (2.4.2) bootsnap (1.9.3) msgpack (~> 1.0) @@ -67,6 +69,8 @@ GEM byebug (11.1.3) coderay (1.1.3) concurrent-ruby (1.1.9) + crack (0.4.5) + rexml crass (1.0.6) diff-lcs (1.4.4) docile (1.4.0) @@ -82,8 +86,13 @@ GEM railties (>= 5.0.0) faker (2.19.0) i18n (>= 1.6, < 2) + faraday (2.2.0) + faraday-net_http (~> 2.0) + ruby2_keywords (>= 0.0.4) + faraday-net_http (2.0.1) globalid (1.0.0) activesupport (>= 5.0) + hashdiff (1.0.1) i18n (1.8.11) concurrent-ruby (~> 1.0) importmap-rails (1.0.1) @@ -120,6 +129,7 @@ GEM pry-byebug (3.9.0) byebug (~> 11.0) pry (~> 0.13.0) + public_suffix (4.0.6) puma (5.5.2) nio4r (~> 2.0) racc (1.6.0) @@ -199,6 +209,7 @@ GEM rubocop-rspec (2.8.0) rubocop (~> 1.19) ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) shoulda-matchers (5.0.0) activesupport (>= 5.2.0) simplecov (0.21.2) @@ -223,6 +234,10 @@ GEM tzinfo (2.0.4) concurrent-ruby (~> 1.0) unicode-display_width (2.1.0) + webmock (3.14.0) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -238,6 +253,7 @@ DEPENDENCIES dotenv-rails factory_bot_rails faker + faraday importmap-rails jbuilder pg (~> 1.1) @@ -256,6 +272,7 @@ DEPENDENCIES sprockets-rails stimulus-rails turbo-rails + webmock RUBY VERSION ruby 3.0.3p157 diff --git a/app/concepts/project/operation/create_remix.rb b/app/concepts/project/operation/create_remix.rb index 298ed8769..7a146e348 100644 --- a/app/concepts/project/operation/create_remix.rb +++ b/app/concepts/project/operation/create_remix.rb @@ -5,29 +5,29 @@ module Operation class CreateRemix require 'operation_response' - def self.call(params) + def self.call(params, user_id) response = OperationResponse.new - validate_params(response, params) + validate_params(response, params, user_id) return response if response.failure? - remix_project(response, params) + remix_project(response, params, user_id) response end class << self private - def validate_params(response, params) - valid = params[:phrase_id].present? && params[:remix][:user_id].present? + def validate_params(response, params, user_id) + valid = params[:phrase_id].present? && user_id.present? response[:error] = 'Invalid parameters' unless valid end - def remix_project(response, params) + def remix_project(response, params, user_id) original_project = Project.find_by!(identifier: params[:phrase_id]) response[:project] = original_project.dup.tap do |proj| - proj.user_id = params[:remix][:user_id] + proj.user_id = user_id proj.components = original_project.components.map(&:dup) end diff --git a/app/controllers/api/projects/remixes_controller.rb b/app/controllers/api/projects/remixes_controller.rb index 38a0c530b..eb25f0a46 100644 --- a/app/controllers/api/projects/remixes_controller.rb +++ b/app/controllers/api/projects/remixes_controller.rb @@ -3,8 +3,10 @@ module Api module Projects class RemixesController < ApiController + before_action :require_oauth_user + def create - result = Project::Operation::CreateRemix.call(remix_params) + result = Project::Operation::CreateRemix.call(remix_params, oauth_user_id) if result.success? @project = result[:project] @@ -17,7 +19,7 @@ def create private def remix_params - params.permit(:phrase_id, remix: [:user_id]) + params.permit(:phrase_id) end end end diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 878bfd879..1a43feefd 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -1,4 +1,19 @@ # frozen_string_literal: true class ApiController < ActionController::API + include OauthUser + + unless Rails.application.config.consider_all_requests_local + rescue_from ActiveRecord::RecordNotFound, with: -> { return404 } + end + + private + + def require_oauth_user + head :unauthorized unless oauth_user_id + end + + def return404 + render json: { error: '404 Not found' }, status: :not_found + end end diff --git a/app/helpers/oauth_user.rb b/app/helpers/oauth_user.rb new file mode 100644 index 000000000..22cd9ecd0 --- /dev/null +++ b/app/helpers/oauth_user.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module OauthUser + def oauth_user_id + @oauth_user_id ||= fetch_oauth_user_id + end + + private + + def fetch_oauth_user_id + return nil if request.headers['Authorization'].blank? + + json = hydra_request + json['sub'] + end + + def hydra_request + con = Faraday.new ENV.fetch('HYDRA_ADMIN_URL') + res = con.post do |req| + req.url '/oauth2/introspect' + req.headers['Content-Type'] = 'application/x-www-form-urlencoded' + req.headers['Authorization'] = "Basic #{ENV.fetch('HYDRA_SECRET')}" + req.body = { token: request.headers['Authorization'] } + end + JSON.parse(res.body) + end +end diff --git a/config/environments/test.rb b/config/environments/test.rb index e67797f21..94716f034 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -25,7 +25,7 @@ } # Show full error reports and disable caching. - config.consider_all_requests_local = true + config.consider_all_requests_local = false config.action_controller.perform_caching = false config.cache_store = :null_store diff --git a/spec/concepts/project/operation/create_remix_spec.rb b/spec/concepts/project/operation/create_remix_spec.rb index 780f97214..9cc6a6c69 100644 --- a/spec/concepts/project/operation/create_remix_spec.rb +++ b/spec/concepts/project/operation/create_remix_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe Project::Operation::CreateRemix, type: :unit do - subject(:create_remix) { described_class.call(params) } + subject(:create_remix) { described_class.call(params, user_id) } let(:user_id) { 'e0675b6c-dc48-4cd6-8c04-0f7ac05af51a' } let!(:original_project) { create(:project, :with_components) } @@ -14,7 +14,7 @@ describe '.call' do context 'when all params valid' do - let(:params) { { phrase_id: original_project.identifier, remix: { user_id: user_id } } } + let(:params) { { phrase_id: original_project.identifier } } it 'returns success' do result = create_remix @@ -53,7 +53,8 @@ end context 'when user_id is not present' do - let(:params) { { phrase_id: original_project.identifier, remix: { user_id: '' } } } + let(:user_id) { nil } + let(:params) { { phrase_id: original_project.identifier } } it 'returns failure' do result = create_remix diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index c2f7fa72e..5da0d50f2 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# This file is copied to spec/ when you run 'rails generate rspec:install' require 'simplecov' SimpleCov.start 'rails' do enable_coverage :branch @@ -9,10 +8,13 @@ require 'spec_helper' ENV['RAILS_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 'webmock/rspec' Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } @@ -71,6 +73,7 @@ # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") config.include PhraseIdentifierMock + config.include OauthUserMock end Shoulda::Matchers.configure do |config| diff --git a/spec/request/projects/remix_spec.rb b/spec/request/projects/remix_spec.rb index 2b7805e19..1f1a62c46 100644 --- a/spec/request/projects/remix_spec.rb +++ b/spec/request/projects/remix_spec.rb @@ -11,19 +11,29 @@ mock_phrase_generation end - it 'returns expected response' do - post "/api/projects/phrases/#{original_project.identifier}/remix", - params: { remix: { user_id: user_id } } + context 'when auth is correct' do + before do + mock_oauth_user + end + + it 'returns success response' do + post "/api/projects/phrases/#{original_project.identifier}/remix" + + expect(response.status).to eq(200) + end + + it 'returns 404 response if invalid project' do + post '/api/projects/phrases/no-such-project/remix' - expect(response.status).to eq(200) + expect(response.status).to eq(404) + end end - context 'when request is invalid' do - it 'returns error response' do - post "/api/projects/phrases/#{original_project.identifier}/remix", - params: { remix: { user_id: '' } } + context 'when auth is invalid' do + it 'returns unauthorized' do + post "/api/projects/phrases/#{original_project.identifier}/remix" - expect(response.status).to eq(400) + expect(response.status).to eq(401) end end end diff --git a/spec/support/oauth_user_mock.rb b/spec/support/oauth_user_mock.rb new file mode 100644 index 000000000..59234bc94 --- /dev/null +++ b/spec/support/oauth_user_mock.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module OauthUserMock + def mock_oauth_user(user_id = nil) + user_id ||= SecureRandom.uuid + + # rubocop:disable RSpec/AnyInstance + allow_any_instance_of(OauthUser).to receive(:oauth_user_id).and_return(user_id) + # rubocop:enable RSpec/AnyInstance + end +end