{"id":16908410,"url":"https://github.com/lanej/critic","last_synced_at":"2025-07-16T18:06:17.502Z","repository":{"id":138430370,"uuid":"43017345","full_name":"lanej/critic","owner":"lanej","description":"Ruby resource authorization framework","archived":false,"fork":false,"pushed_at":"2020-05-20T03:37:00.000Z","size":65,"stargazers_count":1,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-07-05T22:42:23.279Z","etag":null,"topics":["authorization","authorization-framework","conventions","critic","rails","ruby","sinatra"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lanej.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2015-09-23T17:51:09.000Z","updated_at":"2020-05-20T03:37:02.000Z","dependencies_parsed_at":null,"dependency_job_id":"e81f8e4e-c687-41a6-8140-ea45d23adfe6","html_url":"https://github.com/lanej/critic","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/lanej/critic","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lanej%2Fcritic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lanej%2Fcritic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lanej%2Fcritic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lanej%2Fcritic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lanej","download_url":"https://codeload.github.com/lanej/critic/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lanej%2Fcritic/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265529045,"owners_count":23782805,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authorization","authorization-framework","conventions","critic","rails","ruby","sinatra"],"created_at":"2024-10-13T18:51:22.225Z","updated_at":"2025-07-16T18:06:17.497Z","avatar_url":"https://github.com/lanej.png","language":"Ruby","readme":"# Critic\n\nCritic inserts an easily verifiable authorization layer into your MVC application using resource policies.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'critic'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install critic\n\n## Usage\n\n### Policies\n\nA policy contains authorization logic for a resource and an authenticated subject.\n\n```ruby\n# app/policies/post_policy.rb\nclass PostPolicy\n  include Critic::Policy\nend\n```\n\nThere are two types of methods:\n\n* *action* - determines if subject is authorized to perform a specific operation on the resource\n* *scope* - returns a list of resources available to the subject\n\nThe default scope is `index` but it can be overridden by specifying `.scope`.\n\n```ruby\n# app/policies/post_policy.rb\nclass PostPolicy\n  include Critic::Policy\n\n  # set default scope\n  self.scope = :author_index\n\n  # now default scope\n  def author_index\n    resource.where(author_id: subject.id)\n  end\n\n  # no longer the default scope\n  def index\n    resource.order(:created_at)\n  end\nend\n```\n\n#### Actions\n\nThe most basic actions return `true` or `false` to indicate the authorization status.\n\n```ruby\n# app/policies/post_policy.rb\nclass PostPolicy\n  include Critic::Policy\n\n  def update?\n    !resource.locked? \u0026\u0026\n      resource.published_at.present?\n  end\nend\n```\n\nThis policy will only allow updates if the post is not `locked`.\n\nVerify authorization using `#authorize`.\n\n```ruby\nPost = Struct.new(:locked)\nUser = Struct.new\n\nPostPolicy.authorize(:update?, User.new, Post.new(false)).granted? #=\u003e true\nPostPolicy.authorize(:update?, User.new, Post.new(true)).granted? #=\u003e false\n```\n\n#### Authorization Result\n\nReturning a String from your action is interpreted as a failure.  The String is added to the messages of the authorization.\n\n```ruby\nPost = Struct.new(:author_id)\nUser = Struct.new(:id)\n\nclass PostPolicy\n  include Critic::Policy\n\n  def destroy?\n    return true if resource.author_id == subject.id\n    \"Cannot destroy Post: This post is authored by #{resource.author_id}\"\n  end\nend\n\nauthorization = PostPolicy.authorize(destroy?, User.new(1), Post.new(2))\nauthorization.granted? #=\u003e false\nauthorization.messages #=\u003e [\"Cannot destroy Post: This post is authored by 2\"']\n```\n\n`halt` can be used to indicate early failure.  The argument provided to `halt` becomes the result of the authorization.\n\n```ruby\nPost = Struct.new(:author_id)\nUser = Struct.new(:id)\n\nclass PostPolicy\n  include Critic::Policy\n\n  def destroy?\n    if resource.author_id != subject.id\n      halt \"Cannot destroy Post: This post is authored by #{resource.author_id}\"\n    end\n    true\n  end\nend\n\nauthorization = PostPolicy.authorize(destroy?, User.new(1), Post.new(2))\nauthorization.granted? #=\u003e false\nauthorization.messages #=\u003e [\"Cannot destroy Post: This post is authored by 2\"']\n```\n\n`halt(true)` indicates immediate success.\n\n```ruby\nPost = Struct.new(:author_id)\nUser = Struct.new(:id)\n\nclass PostPolicy\n  include Critic::Policy\n\n  def destroy?\n    check_ownership\n    false\n  end\n\n  private\n\n  def check_ownership\n    halt(true) if resource.author_id == subject.id\n  end\nend\n\nauthorization = PostPolicy.authorize(destroy?, User.new(1), Post.new(2))\nauthorization.granted? #=\u003e false\nauthorization.messages #=\u003e [\"Cannot destroy Post: This post is authored by 2\"']\n```\n\n#### Scopes\n\nScopes treat `resource` as a starting point and return a restricted set of associated resources.  Policies can have any number of scopes.  The default scope is `#index`.\n\n```ruby\n# app/policies/post_policy.rb\nclass PostPolicy\n  include Critic::Policy\n\n  def index\n    resource.where(deleted_at: nil, author_id: subject.id)\n  end\nend\n```\n\nVerify authorization using `#authorize`.\n\n```ruby\nPost = Class.new(ActiveRecord::Base)\nUser = Struct.new\n\nauthorization = PostPolicy.authorize(index, User.new, Post.new(false))\nauthorization.granted? #=\u003e true\nauthorization.result #=\u003e \u003c#ActiveRecord::Relation..\u003e\n```\n\n#### Convention\n\nIt can be a useful convention to add a `?` suffix to your action methods.  This allows a clear separation between actions and scopes.  All other methods should be `protected`, similar to Rails controller.\n\n```ruby\n# app/policies/post_policy.rb\nclass PostPolicy\n  include Critic::Policy\n\n  # default scope\n  def index\n    resource.where(published: true)\n  end\n\n  # custom scope\n  def author_index\n    resource.where(author_id: subject.id)\n  end\n\n  # action\n  def show?\n    (post.draft? \u0026\u0026 authored_post?) || post.published?\n  end\n\n  protected\n\n  alias post resource\n\n  def authored_post?\n    subject == post.author\n  end\nend\n```\n\n### Controller\n\nControllers are the primary consumer of policies.  Controllers ask the policy if an authenticated subject is authorized to perform a specific action on a specific resource.\n\n#### Actions\n\nIn Rails, the policy action is inferred from `params[:action]` which corresponds to the controller action method name.\n\nWhen `authorize` fails, a `Critic::AuthorizationDenied` exception is raised with reference to the performed authorization.\n\n```ruby\n# app/controllers/post_controller.rb\nclass PostController \u003c ApplicationController\n  include Critic::Controller\n\n  rescue_from Critic::AuthorizationDenied do |exception|\n    messages = exception.authorization.messages || exception.message\n    render json: {errors: [messages]}, status: :unauthorized\n  end\n\n  def update\n    post = Post.find(params[:id])\n    authorize post # calls PostPolicy#update\n\n    render json: post\n  end\nend\n```\n\nWhen action cannot be inferred, pass the intended action to `authorize`.\n\n```ruby\n# app/controllers/post_controller.rb\nclass PostController \u003c Sinatra::Base\n  include Critic::Controller\n\n  error Critic::AuthorizationDenied do |exception|\n    messages = exception.authorization.messages || exception.message\n\n    body {errors: [*messages]}\n    halt 403\n  end\n\n  put '/posts/:id' do |id|\n    post = Post.find(id)\n    authorize post, :update\n\n    post.to_json\n  end\nend\n```\n\n##### Gentle\n\nCalling `authorized?` returns `true` or `false` instead of raising an exception.\n\n```ruby\n# app/controllers/post_controller.rb\nclass PostController \u003c Sinatra::Base\n  include Critic::Controller\n\n  put '/posts/:id' do |id|\n    post = Post.find(id)\n\n    halt(403) unless authorized?(post, :update)\n\n    post.to_json\n  end\nend\n```\n\n##### Verify authorization\n\n`verify_authorized` enforces that the request was authorized before the response is returned.  A `Critic::AuthorizationMissing` error is raised in this case.  A request is authorized if `authorized?`, `authorize` or `authorizing!` is called before the response is returned.\n\n```ruby\n# app/controllers/post_controller.rb\nclass PostController \u003c Sinatra::Base\n  include Critic::Controller\n\n  verify_authorized\n\n  error Critic::AuthorizationMissing do |exception|\n    # notify developers that something has gone horribly wrong\n    halt 503\n  end\n\n  put '/posts/:id' do |id|\n    post = Post.find(id)\n\n    post.to_json\n  end\nend\n```\n\nThis check can be artificially skipped calling `authorizing!`.\n\n```ruby\n# app/controllers/invitation_controller.rb\nclass InvitationController \u003c Sinatra::Base\n  include Critic::Controller\n\n  verify_authorized\n\n  post '/invitation/accept/code' do |code|\n    invitation = Invitiation.find_by(code: code)\n\n    invitation.accept!\n    authorizing! # Skip authorization check\n\n    redirect '/'\n  end\nend\n```\n\n#### Scopes\n\nUse `authorize_scope` and provide the base scope.  The return value is the result.\n\n```ruby\n# app/controllers/post_controller.rb\nclass PostController \u003c Sinatra::Base\n  include Critic::Controller\n\n  get '/customers/:customer_id/posts' do |customer_id|\n    posts =\n      authorize_scope(Post.where(customer_id: customer_id))\n\n    posts.to_json\n  end\nend\n```\n\nCustom indexes can be used by passing an `action` parameter.\n\n```ruby\n# app/controllers/post_controller.rb\nclass PostController \u003c Sinatra::Base\n  include Critic::Controller\n\n  get '/posts' d\n    posts =\n      authorize_scope(Post, action: :custom_index)\n\n    posts.to_json\n  end\nend\n```\n\n#### Custom subject\n\nBy default, the policy's subject is referenced by `current_user`.  Override `critic` to customize.\n\n```ruby\n# app/controllers/application_controller.rb\nclass ApplicationController \u003c ActionController::Base\n  include Critic::Controller\n\n  protected\n\n  def critic\n    token\n  end\nend\n```\n\n#### Custom policy\n\nThe default policy for a resource is referenced by the resoure class name.  For instance, Critic will look for a `PostPolicy` for a `Post.new` object.  You can set a custom policy for the entire controller by overriding the `policy` method.\n\n```ruby\n# app/controllers/post_controller.rb\nclass PostController \u003c ActionController::Base\n  include Critic::Controller\n\n  protected\n\n  def policy(_resource)\n    V2::PostPolicy\n  end\nend\n```\n\nYou can also provide a specific policy when calling `authorize`\n\n```ruby\n# app/controllers/post_controller.rb\nclass PostController \u003c ActionController::Base\n  include Critic::Controller\n\n  def show\n    post = Post.find(params[:id])\n    authorize post, policy: V2::PostPolicy\n\n    render json: post\n  end\nend\n```\n\n\n#### Testing\n\n`bundle exec rake spec`\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/critic.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flanej%2Fcritic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flanej%2Fcritic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flanej%2Fcritic/lists"}