module JSONAPI module Authorization # An authorizer is a class responsible for linking JSONAPI operations to # your choice of authorization mechanism. # # This class uses Pundit for authorization. It does not yet support all # the available operations — you can use your own authorizer class instead # if you have different needs. See the README.md for configuration # information. # # Fetching records is the concern of +PunditScopedResource+ which in turn # affects which records end up being passed here. class DefaultPunditAuthorizer attr_reader :user # Creates a new DefaultPunditAuthorizer instance # # ==== Parameters # # * +context+ - The context passed down from the controller layer def initialize(context) @user = JSONAPI::Authorization.configuration.user_context(context) end # GET /resources # # ==== Parameters # # * +source_class+ - The source class (e.g. +Article+ for +ArticleResource+) def find(source_class) ::Pundit.authorize(user, source_class, 'index?') end # GET /resources/:id # # ==== Parameters # # * +source_record+ - The record to show def show(source_record) ::Pundit.authorize(user, source_record, 'show?') end # GET /resources/:id/relationships/other-resources # GET /resources/:id/relationships/another-resource # # A query for a +has_one+ or a +has_many+ association # # ==== Parameters # # * +source_record+ - The record whose relationship is queried # * +related_record+ - The associated +has_one+ record to show or +nil+ # if the associated record was not found. For a +has_many+ association, # this will always be +nil+ def show_relationship(source_record, related_record) ::Pundit.authorize(user, source_record, 'show?') ::Pundit.authorize(user, related_record, 'show?') unless related_record.nil? end # GET /resources/:id/another-resource # # A query for a record through a +has_one+ association # # ==== Parameters # # * +source_record+ - The record whose relationship is queried # * +related_record+ - The associated record to show or +nil+ if the # associated record was not found def show_related_resource(source_record, related_record) ::Pundit.authorize(user, source_record, 'show?') ::Pundit.authorize(user, related_record, 'show?') unless related_record.nil? end # GET /resources/:id/other-resources # # A query for records through a +has_many+ association # # ==== Parameters # # * +source_record+ - The record whose relationship is queried def show_related_resources(source_record) ::Pundit.authorize(user, source_record, 'show?') end # PATCH /resources/:id # # ==== Parameters # # * +source_record+ - The record to be modified # * +related_records_with_context+ - A hash with the association type, # the relationship name, an Array of new related records. def replace_fields(source_record, related_records_with_context) ::Pundit.authorize(user, source_record, 'update?') authorize_related_records(source_record, related_records_with_context) end # POST /resources # # ==== Parameters # # * +source_class+ - The class of the record to be created # * +related_records_with_context+ - A has with the association type, # the relationship name, and an Array of new related records. def create_resource(source_class, related_records_with_context) ::Pundit.authorize(user, source_class, 'create?') related_records_with_context.each do |data| relation_name = data[:relation_name] records = data[:records] relationship_method = "create_with_#{relation_name}?" policy = ::Pundit.policy(user, source_class) if policy.respond_to?(relationship_method) unless policy.public_send(relationship_method, records) raise ::Pundit::NotAuthorizedError, query: relationship_method, record: source_class, policy: policy end else Array(records).each do |record| ::Pundit.authorize(user, record, 'update?') end end end end # DELETE /resources/:id # # ==== Parameters # # * +source_record+ - The record to be removed def remove_resource(source_record) ::Pundit.authorize(user, source_record, 'destroy?') end # PATCH /resources/:id/relationships/another-resource # # A replace request for a +has_one+ association # # ==== Parameters # # * +source_record+ - The record whose relationship is modified # * +new_related_record+ - The new record replacing the old record # * +relationship_type+ - The relationship type def replace_to_one_relationship(source_record, new_related_record, relationship_type) relationship_method = "replace_#{relationship_type}?" authorize_relationship_operation(source_record, relationship_method, new_related_record) end # POST /resources/:id/relationships/other-resources # # A request for adding to a +has_many+ association # # ==== Parameters # # * +source_record+ - The record whose relationship is modified # * +new_related_records+ - The new records to be added to the association # * +relationship_type+ - The relationship type def create_to_many_relationship(source_record, new_related_records, relationship_type) relationship_method = "add_to_#{relationship_type}?" authorize_relationship_operation(source_record, relationship_method, new_related_records) end # PATCH /resources/:id/relationships/other-resources # # A replace request for a +has_many+ association # # ==== Parameters # # * +source_record+ - The record whose relationship is modified # * +new_related_records+ - The new records replacing the entire +has_many+ # association # * +relationship_type+ - The relationship type def replace_to_many_relationship(source_record, new_related_records, relationship_type) relationship_method = "replace_#{relationship_type}?" authorize_relationship_operation(source_record, relationship_method, new_related_records) end # DELETE /resources/:id/relationships/other-resources # # A request to disassociate elements of a +has_many+ association # # ==== Parameters # # * +source_record+ - The record whose relationship is modified # * +related_records+ - The records which will be disassociated from +source_record+ # * +relationship_type+ - The relationship type def remove_to_many_relationship(source_record, related_records, relationship_type) relationship_method = "remove_from_#{relationship_type}?" authorize_relationship_operation(source_record, relationship_method, related_records) end # DELETE /resources/:id/relationships/another-resource # # A request to disassociate a +has_one+ association # # ==== Parameters # # * +source_record+ - The record whose relationship is modified # * +relationship_type+ - The relationship type def remove_to_one_relationship(source_record, relationship_type) relationship_method = "remove_#{relationship_type}?" authorize_relationship_operation(source_record, relationship_method) end # Any request including ?include=other-resources # # This will be called for each has_many relationship if the include goes # deeper than one level until some authorization fails or the include # directive has been travelled completely. # # We can't pass all the records of a +has_many+ association here due to # performance reasons, so the class is passed instead. # # ==== Parameters # # * +source_record+ — The source relationship record, e.g. an Article in # article.comments check # * +record_class+ - The underlying record class for the relationships # resource. def include_has_many_resource(_source_record, record_class) ::Pundit.authorize(user, record_class, 'index?') end # Any request including ?include=another-resource # # This will be called for each has_one relationship if the include goes # deeper than one level until some authorization fails or the include # directive has been travelled completely. # # ==== Parameters # # * +source_record+ — The source relationship record, e.g. an Article in # article.author check # * +related_record+ - The associated record to return def include_has_one_resource(_source_record, related_record) ::Pundit.authorize(user, related_record, 'show?') end private def authorize_relationship_operation(source_record, relationship_method, *args) policy = ::Pundit.policy(user, source_record) if policy.respond_to?(relationship_method) unless policy.public_send(relationship_method, *args) raise ::Pundit::NotAuthorizedError, query: relationship_method, record: source_record, policy: policy end else ::Pundit.authorize(user, source_record, 'update?') end end def authorize_related_records(source_record, related_records_with_context) related_records_with_context.each do |data| relation_type = data[:relation_type] relation_name = data[:relation_name] records = data[:records] case relation_type when :to_many replace_to_many_relationship(source_record, records, relation_name) when :to_one if records.nil? remove_to_one_relationship(source_record, relation_name) else replace_to_one_relationship(source_record, records, relation_name) end end end end end end end