Rails Pundit allow published content to be viewed by others while preventing others from viewing user's info

600 views Asked by At

Okay, so I have a User, Book and Chapter entities in my system.

If an author (User entity) publishes a book as well as a chapter, then it's available for the public to see. Let's call author Jim.

That means if another normal user, named Tycus, wants to read Jim's book and book chapters, he should be able to do so.

I am using Pundit gem (https://github.com/elabs/pundit) for permissions.

The problem I am facing is, when Tycus tries to access Jim's book, it appears my Rails is trying to fetch linked relationships (chapter --> book --> author) along with it:

Started GET "//books/16" for ::1 at 2016-12-16 23:29:38 +0800
Processing by BooksController#show as JSON
  Parameters: {"id"=>"16"}
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 21], ["LIMIT", 1]]
  Book Load (0.1ms)  SELECT  "books".* FROM "books" WHERE "books"."id" = ? LIMIT ?  [["id", 16], ["LIMIT", 1]]
  Role Load (0.1ms)  SELECT  "roles".* FROM "roles" WHERE "roles"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
[active_model_serializers]   User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
[active_model_serializers]   Chapter Load (0.1ms)  SELECT "chapters".* FROM "chapters" WHERE "chapters"."book_id" = ?  [["book_id", 16]]
[active_model_serializers]   Genre Load (0.1ms)  SELECT "genres".* FROM "genres" INNER JOIN "books_genres" ON "genres"."id" = "books_genres"."genre_id" WHERE "books_genres"."book_id" = ?  [["book_id", 16]]
[active_model_serializers]   Love Load (0.1ms)  SELECT "loves".* FROM "loves" WHERE "loves"."book_id" = ?  [["book_id", 16]]
[active_model_serializers] Rendered BookSerializer with ActiveModelSerializers::Adapter::JsonApi (30.04ms)
Completed 200 OK in 48ms (Views: 29.6ms | ActiveRecord: 2.0ms)


Started GET "//chapters/5" for ::1 at 2016-12-16 23:29:38 +0800
Processing by ChaptersController#show as JSON
  Parameters: {"id"=>"5"}
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 21], ["LIMIT", 1]]
  Chapter Load (0.1ms)  SELECT  "chapters".* FROM "chapters" WHERE "chapters"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]
  Book Load (0.1ms)  SELECT  "books".* FROM "books" WHERE "books"."id" = ? LIMIT ?  [["id", 16], ["LIMIT", 1]]
Started GET "//chapters/7" for ::1 at 2016-12-16 23:29:38 +0800
Started GET "//chapters/6" for ::1 at 2016-12-16 23:29:38 +0800
[active_model_serializers]   User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
Processing by ChaptersController#show as JSON
Processing by ChaptersController#show as JSON
Started GET "//users/2" for ::1 at 2016-12-16 23:29:38 +0800
[active_model_serializers] Rendered ChapterSerializer with ActiveModelSerializers::Adapter::JsonApi (10.73ms)
  Parameters: {"id"=>"7"}
  Parameters: {"id"=>"6"}
Processing by UsersController#show as JSON
Completed 200 OK in 24ms (Views: 14.2ms | ActiveRecord: 0.9ms)


  User Load (0.8ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 21], ["LIMIT", 1]]
  User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 21], ["LIMIT", 1]]
  Parameters: {"id"=>"2"}
  Chapter Load (0.2ms)  SELECT  "chapters".* FROM "chapters" WHERE "chapters"."id" = ? LIMIT ?  [["id", 7], ["LIMIT", 1]]
  Chapter Load (0.2ms)  SELECT  "chapters".* FROM "chapters" WHERE "chapters"."id" = ? LIMIT ?  [["id", 6], ["LIMIT", 1]]
  User Load (0.7ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 21], ["LIMIT", 1]]
  Book Load (0.1ms)  SELECT  "books".* FROM "books" WHERE "books"."id" = ? LIMIT ?  [["id", 16], ["LIMIT", 1]]
  Book Load (0.1ms)  SELECT  "books".* FROM "books" WHERE "books"."id" = ? LIMIT ?  [["id", 16], ["LIMIT", 1]]
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
[active_model_serializers]   User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
[active_model_serializers]   User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Role Load (0.2ms)  SELECT  "roles".* FROM "roles" WHERE "roles"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
[active_model_serializers] Rendered ChapterSerializer with ActiveModelSerializers::Adapter::JsonApi (6.75ms)
[active_model_serializers] Rendered ChapterSerializer with ActiveModelSerializers::Adapter::JsonApi (5.92ms)
Completed 403 Forbidden in 16ms (ActiveRecord: 1.0ms)


Completed 200 OK in 22ms (Views: 9.9ms | ActiveRecord: 1.6ms)


Completed 200 OK in 20ms (Views: 7.9ms | ActiveRecord: 1.0ms)



Pundit::NotAuthorizedError (not allowed to show? this #<User id: 2, first_name: "James", last_name: "Raynor", username: "Jimmy", email: "[email protected]", password_digest: "$2a$10$9xCQKiku7YD.xjzbj34/P.4JUHCOf4lKXbVeqKy2PNb...", banned: false, role_id: 3, created_at: "2016-12-01 13:56:30", updated_at: "2016-12-11 08:31:42", photo: "jim_raynor.jpg", email_confirmed: true, confirm_token: nil, password_reset_token: nil>):

app/controllers/users_controller.rb:33:in `show'

As a result, Pundit is raising a Pundit::NotAuthorizedError because somehow it thinks I'm trying to access the user's information.

My Emberjs frontend rightfully resonate with this exception raised:

Ember error

My Chapter_Controller certainly don't explicitly ask for the author's info:

def show
  chapter = Chapter.find_by(id: params[:id])

  if chapter.present?
    authorize chapter
    render json: chapter
  else
    skip_authorization
    render status: :not_found
  end
end

I can fix this error by modify my User policy show? method to return true:

def show?
    true
end

My show? method is currently like this:

def show?
    # Allowing admins to view other admins (but do not allow update or deleting other admins)
    if @user.superuser? || @record.id == @user.id || (@user.admin? && [email protected]?)
      return true
    elsif (@record.id != @user.id)
      return false
    end
end

But this then exposes my user's information to anyone to see. For example, let's say an author does not want to disclose their real name, only their username (maybe the author isn't too confident his/her book will sell well, so using an alias username to hide their identity).

By specifying true in my User policy show method, any logged in user can make a GET request to: http://localhost:3000/users/{author_id} and see the author's details.

So my question is - is there a way to allow other user to view book and book chapters of an author but at the same time do not allow other users to view the author's personal info?

Update

It appears my active model serializer is the one trying to pull the user record.

I think a similar discussion is happening on the active model serializer github pages: https://github.com/rails-api/active_model_serializers/issues/1552

Update 2 - Chapter Policy Show Method

def show?
  # superuser and admins should respect author's privacy
  # and not be able to view author's unpublished works
  owner? || @record.published
end

def owner?
  @record.book.author_id == @user.id
end

Chapter belongs to a Book and a Book belongs to a User.

How is ChapterPolicy#show defined? Is that method referencing the UserPolicy class? (If so, don't do that!)

Okay...how else can I limit the unpublished chapters to be visible only to the author of the book if I don't check if the currently logged in user is the author?

In case it's any help, my Active Model Serializer for Chapter, Book and User are as follow:

Chapter:

class ChapterSerializer < ActiveModel::Serializer
  attributes :id, :title, :order, :content, :published, :picture
  attribute :content, if: :content_author?

  belongs_to :book

  def content_author?
    # ---------------------------------------------------------
    # Only author of the content can view their unpublished
    # chapter content. Other users including superuser, admin
    # and other normal users should not be able to view
    # author's unpublished chapter content, even during
    # admin/superuser updating author's chapter operation.
    #
    # We want to respect author's privacy and entitlement
    # to publishing their story whenever they feel it's ready.
    # ---------------------------------------------------------
    if current_user != object.book.author && !object.published
      return false
    else
      return true
    end
  end
end

Book:

class BookSerializer < ActiveModel::Serializer
  attributes :id, :title, :blurb, :adult_content, :published, :cover

  belongs_to :author, class_name: "User"
  has_many :chapters
  has_many :genres
  has_many :loves
end

User:

class UserSerializer < ActiveModel::Serializer
  attributes :id, :first_name, :last_name, :username, :email, :banned, :photo

  belongs_to :role
  has_many :friends
end
1

There are 1 answers

0
Tom Lord On BEST ANSWER

Your issue is that due to how your models are related:

class ChapterSerializer < ActiveModel::Serializer
  belongs_to :book
end

class BookSerializer < ActiveModel::Serializer
  belongs_to :author, class_name: "User"
end

Somewhere in your front-end application, you are requesting information about the book's author - it's unclear exactly what information this is, but for example you could perhaps be looking up "other books by this author"?

This is triggering a GET request to /users/:id, which returns a 403 error due to your implementation of UserPolicy#show?.

There is no single answer to your problem, as it is somewhat an architectural design issue/decision that you need to make. But for example, possible approach would be to allow users to always be viewable:

class UserPolicy
  def show?
    true
  end
end

...And in your UserSerializer, conditionally define which attributes get returned, based upon the current_user.