diff --git a/Gemfile b/Gemfile index 31cf1ca7..0b16ca46 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby "3.2.2" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem "rails", "~> 7.0.8" +gem "rails", "~> 7.1.2" gem "config" diff --git a/Gemfile.lock b/Gemfile.lock index 144f20a8..78b260df 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,74 +9,85 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8) - actionpack (= 7.0.8) - activesupport (= 7.0.8) + actioncable (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8) - actionpack (= 7.0.8) - activejob (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + zeitwerk (~> 2.6) + actionmailbox (7.1.2) + actionpack (= 7.1.2) + activejob (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8) - actionpack (= 7.0.8) - actionview (= 7.0.8) - activejob (= 7.0.8) - activesupport (= 7.0.8) + actionmailer (7.1.2) + actionpack (= 7.1.2) + actionview (= 7.1.2) + activejob (= 7.1.2) + activesupport (= 7.1.2) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp - rails-dom-testing (~> 2.0) - actionpack (7.0.8) - actionview (= 7.0.8) - activesupport (= 7.0.8) - rack (~> 2.0, >= 2.2.4) + rails-dom-testing (~> 2.2) + actionpack (7.1.2) + actionview (= 7.1.2) + activesupport (= 7.1.2) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8) - actionpack (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.2) + actionpack (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8) - activesupport (= 7.0.8) + actionview (7.1.2) + activesupport (= 7.1.2) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.8) - activesupport (= 7.0.8) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.2) + activesupport (= 7.1.2) globalid (>= 0.3.6) - activemodel (7.0.8) - activesupport (= 7.0.8) - activerecord (7.0.8) - activemodel (= 7.0.8) - activesupport (= 7.0.8) - activestorage (7.0.8) - actionpack (= 7.0.8) - activejob (= 7.0.8) - activerecord (= 7.0.8) - activesupport (= 7.0.8) + activemodel (7.1.2) + activesupport (= 7.1.2) + activerecord (7.1.2) + activemodel (= 7.1.2) + activesupport (= 7.1.2) + timeout (>= 0.4.0) + activestorage (7.1.2) + actionpack (= 7.1.2) + activejob (= 7.1.2) + activerecord (= 7.1.2) + activesupport (= 7.1.2) marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (7.0.8) + activesupport (7.1.2) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) acts_as_list (1.1.0) activerecord (>= 4.2) ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.5) bootsnap (1.17.0) msgpack (~> 1.2) brakeman (6.1.0) @@ -88,14 +99,17 @@ GEM config (5.0.0) deep_merge (~> 1.2, >= 1.2.1) dry-validation (~> 1.0, >= 1.0.0) + connection_pool (2.4.1) crass (1.0.6) - date (3.3.3) + date (3.3.4) debug (1.9.0) irb (~> 1.10) reline (>= 0.3.8) deep_merge (1.2.2) diff-lcs (1.5.0) docile (1.4.0) + drb (2.2.0) + ruby2_keywords dry-configurable (1.1.0) dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) @@ -160,18 +174,18 @@ GEM net-pop net-smtp marcel (1.0.2) - method_source (1.0.0) mini_mime (1.1.5) minitest (5.20.0) msgpack (1.7.2) - net-imap (0.3.7) + mutex_m (0.2.0) + net-imap (0.4.8) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout - net-smtp (0.3.3) + net-smtp (0.4.0) net-protocol nio4r (2.5.9) nokogiri (1.15.5-arm64-darwin) @@ -194,22 +208,27 @@ GEM nio4r (~> 2.0) racc (1.7.3) rack (2.2.8) + rack-session (1.0.2) + rack (< 3) rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8) - actioncable (= 7.0.8) - actionmailbox (= 7.0.8) - actionmailer (= 7.0.8) - actionpack (= 7.0.8) - actiontext (= 7.0.8) - actionview (= 7.0.8) - activejob (= 7.0.8) - activemodel (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + rackup (1.0.0) + rack (< 3) + webrick + rails (7.1.2) + actioncable (= 7.1.2) + actionmailbox (= 7.1.2) + actionmailer (= 7.1.2) + actionpack (= 7.1.2) + actiontext (= 7.1.2) + actionview (= 7.1.2) + activejob (= 7.1.2) + activemodel (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) bundler (>= 1.15.0) - railties (= 7.0.8) + railties (= 7.1.2) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -217,13 +236,14 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.0.8) - actionpack (= 7.0.8) - activesupport (= 7.0.8) - method_source + railties (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) + irb + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.1.0) rdoc (6.6.1) @@ -288,6 +308,7 @@ GEM rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) sentry-rails (5.15.0) railties (>= 5.0) sentry-ruby (~> 5.15.0) @@ -301,12 +322,13 @@ GEM simplecov_json_formatter (0.1.4) stringio (3.1.0) thor (1.3.0) - timeout (0.4.0) + timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) tzinfo-data (1.2023.3) tzinfo (>= 1.0.0) unicode-display_width (2.5.0) + webrick (1.8.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -333,7 +355,7 @@ DEPENDENCIES paper_trail pg (~> 1.5) puma (~> 6.4) - rails (~> 7.0.8) + rails (~> 7.1.2) reverse_markdown (~> 2.1) rspec-rails rubocop-govuk diff --git a/app/controllers/api/v1/access_tokens_controller.rb b/app/controllers/api/v1/access_tokens_controller.rb index eb494266..2060d761 100644 --- a/app/controllers/api/v1/access_tokens_controller.rb +++ b/app/controllers/api/v1/access_tokens_controller.rb @@ -32,7 +32,7 @@ def caller_identity private def token_params - params.permit(:owner, :description) + params.permit(:owner, :description, :permissions) end def token_deactivate_params diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2fb9c351..1dae17e4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,5 @@ class ApplicationController < ActionController::API - include ActionController::HttpAuthentication::Token::ControllerMethods + include ActionController::HttpAuthentication::Token rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActiveRecord::RecordInvalid, with: :invalid_record @@ -38,20 +38,11 @@ def authenticate_using_old_env_vars end def authenticate_using_access_tokens - if request.headers["X-Api-Token"].present? - token = request.headers["X-Api-Token"] + (request.headers["X-Api-Token"].presence || token_and_options(request)&.first).try! do |token| @access_token = AccessToken.active.find_by_token_digest(Digest::SHA256.hexdigest(token)) - if @access_token.present? - @access_token.update!(last_accessed_at: Time.zone.now) - true - else - false - end - else - authenticate_with_http_token do |token| - @access_token = AccessToken.active.find_by_token_digest(Digest::SHA256.hexdigest(token)) - @access_token.update!(last_accessed_at: Time.zone.now) if @access_token.present? - end + return nil unless @access_token.present? && AccessTokenPolicy.new(@access_token, request).request? + + @access_token.update!(last_accessed_at: Time.zone.now) end end diff --git a/app/models/access_token.rb b/app/models/access_token.rb index 7244d999..28c80f7b 100644 --- a/app/models/access_token.rb +++ b/app/models/access_token.rb @@ -3,6 +3,11 @@ class AccessToken < ApplicationRecord scope :active, -> { where(deactivated_at: nil) } + enum :permissions, { + all: "all", + readonly: "readonly", + }, suffix: true, validate: true + def generate_token users_token = SecureRandom.uuid self.token_digest = Digest::SHA256.hexdigest(users_token) diff --git a/app/policies/access_token_policy.rb b/app/policies/access_token_policy.rb new file mode 100644 index 00000000..f409a4f8 --- /dev/null +++ b/app/policies/access_token_policy.rb @@ -0,0 +1,12 @@ +class AccessTokenPolicy + attr_reader :access_token, :request + + def initialize(access_token, request) + @access_token = access_token + @request = request + end + + def request? + access_token.all_permissions? || request.get? + end +end diff --git a/config/application.rb b/config/application.rb index 9b02cfdd..9cca7158 100644 --- a/config/application.rb +++ b/config/application.rb @@ -22,7 +22,12 @@ module FormsApi class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.0 + config.load_defaults 7.1 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) # Configuration for the application, engines, and railties goes here. # @@ -45,11 +50,8 @@ class Application < Rails::Application # Use JSON log formatter for better support in Splunk. To use conventional # logging use the Logger::Formatter.new. config.log_formatter = JsonLogFormatter.new - - if ENV["RAILS_LOG_TO_STDOUT"].present? - config.logger = ActiveSupport::Logger.new($stdout) - config.logger.formatter = config.log_formatter - end + config.logger = ActiveSupport::Logger.new($stdout) + config.logger.formatter = config.log_formatter # Lograge is used to format the standard HTTP request logging config.lograge.enabled = true diff --git a/config/environments/development.rb b/config/environments/development.rb index e84c5d98..e4cca7e2 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -53,6 +53,9 @@ # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true @@ -62,6 +65,9 @@ # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true + # Add docker hostname for connecting from docker containers to host # so we can run other components in docker and the API locally config.hosts << ".host.docker.internal" diff --git a/config/environments/production.rb b/config/environments/production.rb index 3721efaf..b016323c 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -15,13 +15,12 @@ # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false - # 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). + # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment + # key such as config/credentials/production.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? + # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. + # config.public_file_server.enabled = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" @@ -38,8 +37,25 @@ # config.action_cable.url = "wss://example.com/cable" # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + config.assume_ssl = true + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true + config.force_ssl = true + + # Do not enable log_tags because it interferes with the + # json formatting of log_rage. The request_id is already + # being logged by log_rage. + # config.log_tags = [:request_id] + + # Prepend all log lines with the following tags. + # config.log_tags = [:request_id] + + # Info include generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") # Use a different cache store in production. # config.cache_store = :mem_cache_store @@ -63,4 +79,12 @@ # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } end diff --git a/config/environments/test.rb b/config/environments/test.rb index 22f9fc0c..3fdd761b 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -8,12 +8,13 @@ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # Turn false under Spring and add config.action_view.cache_template_loading = true. - config.cache_classes = true + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false - # Eager loading loads your whole application. When running a single test locally, - # this probably isn't necessary. It's a good idea to do in a continuous integration - # system, or in some way before deploying your code. + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. config.eager_load = ENV["CI"].present? # Configure public file server for tests with Cache-Control for performance. @@ -27,8 +28,8 @@ config.action_controller.perform_caching = false config.cache_store = :null_store - # Raise exceptions instead of rendering exception templates. - config.action_dispatch.show_exceptions = false + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false @@ -57,4 +58,7 @@ # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 166997c5..262e8620 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,8 +1,8 @@ # Be sure to restart your server when you modify this file. -# Configure parameters to be filtered from the log file. Use this to limit dissemination of -# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported -# notations and behaviors. +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. Rails.application.config.filter_parameters += %i[ passw secret token _key crypt salt certificate otp ssn ] diff --git a/db/migrate/20231218125427_add_permissions_to_access_tokens.rb b/db/migrate/20231218125427_add_permissions_to_access_tokens.rb new file mode 100644 index 00000000..995db363 --- /dev/null +++ b/db/migrate/20231218125427_add_permissions_to_access_tokens.rb @@ -0,0 +1,5 @@ +class AddPermissionsToAccessTokens < ActiveRecord::Migration[7.1] + def change + add_column :access_tokens, :permissions, :string, default: "all" + end +end diff --git a/db/schema.rb b/db/schema.rb index 464cd3ee..7b514f00 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[7.0].define(version: 2023_12_06_162240) do +ActiveRecord::Schema[7.1].define(version: 2023_12_18_125427) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -22,6 +22,7 @@ t.datetime "updated_at", null: false t.datetime "last_accessed_at" t.string "description" + t.string "permissions", default: "all" end create_table "conditions", force: :cascade do |t| diff --git a/spec/factories/access_tokens.rb b/spec/factories/access_tokens.rb index 33b11229..cb8f4886 100644 --- a/spec/factories/access_tokens.rb +++ b/spec/factories/access_tokens.rb @@ -2,6 +2,7 @@ factory :access_token do token_digest { Faker::Crypto.sha256 } owner { Faker::Name.first_name.underscore } + permissions { :all } deactivated_at { nil } description { nil } last_accessed_at { nil } diff --git a/spec/integration/access_tokens_spec.rb b/spec/integration/access_tokens_spec.rb new file mode 100644 index 00000000..3f00d147 --- /dev/null +++ b/spec/integration/access_tokens_spec.rb @@ -0,0 +1,98 @@ +require "rails_helper" + +RSpec.describe "access control using access tokens" do + context "when auth is enabled" do + before do + allow(Settings.forms_api).to receive(:enabled_auth).and_return(true) + end + + let(:access) do + AccessToken.new(owner: :test) + end + + let!(:token) do + token = access.generate_token + access.save! + token + end + + it "denies access to the API if the request does not include an access token" do + get forms_path + + expect(response).to have_http_status(:unauthorized) + end + + it "denies access to the API if the request includes an invalid access token" do + get forms_path, headers: { Authorization: "Token foobar" } + + expect(response).to have_http_status(:unauthorized) + end + + it "allows access to the API if the request includes an active access token" do + get forms_path, headers: { Authorization: "Token #{token}" } + + expect(response).to have_http_status(:ok) + end + + it "allows access to the API if the request includes an active access token as a bearer token" do + get forms_path, headers: { Authorization: "Bearer #{token}" } + + expect(response).to have_http_status(:ok) + end + + it "denies access to the API if the request includes a deactiveated access token" do + put deactivate_access_token_path(access.id), headers: { AUTHORIZATION: "Token #{token}" } + + get forms_path, headers: { Authorization: "Token #{token}" } + + expect(response).to have_http_status(:unauthorized) + end + + context "when a user has a readonly token" do + let(:access) do + AccessToken.new(owner: :test, permissions: :readonly) + end + + let(:headers) do + { Authorization: "Token #{token}" } + end + + it "allows access to the API for GET requests" do + get(forms_path, headers:) + + expect(response).to have_http_status(:ok) + end + + it "denies access to the API for POST requests" do + post(forms_path, params: { form: { name: "test form" } }, headers:) + + expect(response).to have_http_status(:unauthorized) + expect(Form.last).to be nil + end + + it "denies access to the API for PUT requests" do + form = create :form, id: 1, name: "test form" + + put(form_path(1), params: { form: { name: "edited test form" } }, headers:) + + expect(response).to have_http_status(:unauthorized) + expect(form.name).to eq "test form" + end + + it "denies access to the API for PATCH requests" do + form = create :form, id: 1, name: "test form" + + patch(form_path(1), params: { form: { name: "edited test form" } }, headers:) + + expect(response).to have_http_status(:unauthorized) + expect(form.name).to eq "test form" + end + + it "does not allow creating other access tokens" do + post(access_tokens_path, params: { owner: "test" }, headers:) + + expect(response).to have_http_status(:unauthorized) + end + end + end +end diff --git a/spec/models/access_token_spec.rb b/spec/models/access_token_spec.rb index ec25d21a..78390acc 100644 --- a/spec/models/access_token_spec.rb +++ b/spec/models/access_token_spec.rb @@ -46,6 +46,35 @@ end end + describe "permissions" do + let(:access_token) do + described_class.new(owner: "test") + .tap(&:generate_token) + end + + it "defaults to all permissions" do + expect(access_token.all_permissions?).to be true + end + + it "allows readonly permissions" do + access_token.permissions = :readonly + + expect(access_token).to be_valid + end + + it "validates the permissions are set" do + access_token.permissions = nil + + expect(access_token).not_to be_valid + end + + it "validates the permissions are valid" do + access_token.permissions = :foobar + + expect(access_token).not_to be_valid + end + end + describe "#generate_token" do let(:result) { access_token.generate_token } diff --git a/spec/policies/access_token_policy_spec.rb b/spec/policies/access_token_policy_spec.rb new file mode 100644 index 00000000..e442d8a1 --- /dev/null +++ b/spec/policies/access_token_policy_spec.rb @@ -0,0 +1,53 @@ +require "rails_helper" + +RSpec.describe AccessTokenPolicy do + subject(:policy) { described_class.new(access, request) } + + let(:access) do + build :access_token + end + + let(:request) do + ActionDispatch::Request.empty + end + + describe "#request?" do + before do + request.headers["REQUEST_METHOD"] = request_method + end + + context "when request method is GET" do + let(:request_method) { "GET" } + + it "grants access if access token has all permissions" do + access.permissions = :all + + expect(policy.request?).to be true + end + + it "grants access if access token has readonly permissions" do + access.permissions = :readonly + + expect(policy.request?).to be true + end + end + + (ActionDispatch::Request::HTTP_METHODS - %w[GET]).each do |request_method_| + context "when request method is #{request_method_}" do + let(:request_method) { request_method_ } + + it "grants access if access token has all permissions" do + access.permissions = :all + + expect(policy.request?).to be true + end + + it "denies access if access token has readonly permissions" do + access.permissions = :readonly + + expect(policy.request?).to be false + end + end + end + end +end diff --git a/spec/request/api/v1/access_tokens_controller_spec.rb b/spec/request/api/v1/access_tokens_controller_spec.rb index 858c03b8..fcaca185 100644 --- a/spec/request/api/v1/access_tokens_controller_spec.rb +++ b/spec/request/api/v1/access_tokens_controller_spec.rb @@ -15,6 +15,7 @@ expect(token.keys).to contain_exactly( :id, :owner, + :permissions, :deactivated_at, :description, :created_at, @@ -63,6 +64,36 @@ expect(AccessToken.last.description).to eq "This is one key to rule them all." end end + + context "when specific permissions are requested" do + before do + allow(AccessToken).to receive(:new).and_call_original + post access_tokens_path, params: { owner: "testing user", permissions: :all }, as: :json + end + + it "returns 201 if its saved" do + expect(response).to have_http_status(:created) + end + + it "returns json" do + expect(response.headers["Content-Type"]).to eq("application/json") + end + + it "sets the description" do + expect(AccessToken.last.permissions).to eq "all" + end + end + + context "when invalid permissions are requested" do + before do + allow(AccessToken).to receive(:new).and_call_original + post access_tokens_path, params: { owner: "testing user", permissions: :foobar }, as: :json + end + + it "returns an error code" do + expect(response).to have_http_status(:bad_request) + end + end end describe "#deactivate" do @@ -117,6 +148,7 @@ id: access_token.id, token_digest: access_token.token_digest, owner: access_token.owner, + permissions: access_token.permissions, description: nil, deactivated_at: nil, created_at: access_token.created_at.as_json, diff --git a/spec/request/application_controller_spec.rb b/spec/request/application_controller_spec.rb index d089bf28..5c36e801 100644 --- a/spec/request/application_controller_spec.rb +++ b/spec/request/application_controller_spec.rb @@ -156,6 +156,25 @@ expect(json_body[:status]).to eq("unauthorised") end end + + context "when token with readonly permissions is used for update method" do + let(:access_token) { AccessToken.new(owner: "test-owner", permissions: :readonly) } + + before do + access_token + token + access_token.save! + post forms_path, params: { form: { name: "Test form" } }, headers: req_headers + end + + it "returns 401" do + expect(response.status).to eq(401) + end + + it "returns an error message" do + expect(json_body[:status]).to eq("unauthorised") + end + end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dff5db70..efbd6ed8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,4 @@ +require "active_support" require "active_support/testing/time_helpers" require "simplecov" require_relative "support/features"