Skip to content

Instantly share code, notes, and snippets.

@ezekg
Last active August 12, 2022 14:48
Show Gist options
  • Save ezekg/fa8eb565cf9867a288c8f8c699507baf to your computer and use it in GitHub Desktop.
Save ezekg/fa8eb565cf9867a288c8f8c699507baf to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "rails"
gem "action_policy"
gem "sqlite3"
end
require "rails"
require "action_controller"
require "action_policy"
require "active_record"
##
# Uncomment this monkey patch to remove authorization context memoization:
##
# module ActionPolicy::Behaviour
# ##
# # Remove memoization of authorization context. This allows us to use
# # authorized_scope() before authorize!() in controllers for nested
# # resources, e.g. /posts/:post_id/comments.
# def authorization_context = self.class.authorization_targets.each_with_object({}) { |(k, m), o| o[k] = send(m) }
# end
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :users, force: true do |t|
t.string :role
t.timestamps
end
create_table :posts, force: true do |t|
t.references :user
t.timestamps
end
create_table :comments, force: true do |t|
t.references :post
t.string :message
t.timestamps
end
end
class TestApp < Rails::Application
config.hosts << 'example.org'
secrets.secret_key_base = 'secret_key_base'
config.root = __dir__
config.logger = Logger.new(STDOUT)
config.eager_load = false
Rails.logger = config.logger
routes.append do
resources :posts, only: %i[index show] do
scope module: :posts do
resources :comments, only: %i[index show]
end
end
end
end
TestApp.initialize!
class User < ActiveRecord::Base
has_many :posts
scope :for_user, ->user: { none }
def admin? = role == 'admin'
end
class Post < ActiveRecord::Base
belongs_to :author, class_name: 'User', foreign_key: :user_id
has_many :comments
scope :for_user, ->user: { where(author: user) }
end
class Comment < ActiveRecord::Base
belongs_to :author, class_name: 'User', foreign_key: :user_id
belongs_to :post
scope :for_user, ->user: {
joins(:post).where(author: user).or(
joins(:post).where(post: { author: user }),
)
}
end
class ApplicationPolicy < ActionPolicy::Base
authorize :user
relation_scope do |relation|
next relation if user.admin?
relation.for_user(user:)
end
end
class PostPolicy < ApplicationPolicy; end
class Post::CommentPolicy < ApplicationPolicy
authorize :post
def index?
user.admin? || post.author == user
end
def show?
user.admin? || post.author == user
end
end
class ApplicationController < ActionController::API
include ActionPolicy::Controller
before_action :set_current_user
rescue_from ActiveRecord::RecordNotFound, with: -> { render status: :not_found }
rescue_from ActionPolicy::Unauthorized, with: -> { render status: :forbidden }
authorize :user, through: :current_user
private
attr_reader :current_user
def set_current_user
@current_user = User.find(params[:current_user_id])
end
end
module Posts
class CommentsController < ApplicationController
before_action :set_post
# We want to assert the post is accessible to the current
# user, in addition to any comment records.
authorize :post
def index
comments = post.comments
authorize! comments, with: Post::CommentPolicy
render json: comments
end
def show
comment = post.comments.find(params[:id])
authorize! comment, with: Post::CommentPolicy
render json: comment
end
private
attr_reader :post
def set_post
@post = authorized_scope(Post.all).find(params[:post_id])
end
end
end
require "minitest/autorun"
require "rack/test"
class BugTest < Minitest::Test
include Rack::Test::Methods
def test_list_posts_as_admin
admin = User.create!(role: 'admin')
author = User.create!
post = author.posts.create!
post.comments.create!(message: 'Hello, world!')
get "/posts/#{post.id}/comments", current_user_id: admin.id
assert_equal 200, last_response.status
end
def test_list_posts_as_author
author = User.create!
post = author.posts.create!
post.comments.create!(message: 'Hello, world!')
get "/posts/#{post.id}/comments", current_user_id: author.id
assert_equal 200, last_response.status
end
def test_list_posts_as_user
author = User.create!
reader = User.create!
post = author.posts.create!
post.comments.create!(message: 'Hello, world!')
get "/posts/#{post.id}/comments", current_user_id: reader.id
assert_equal 404, last_response.status
end
private
def app
Rails.application
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment