{"id":13878284,"url":"https://github.com/maximgurin/verifica","last_synced_at":"2025-07-16T14:31:53.823Z","repository":{"id":65369245,"uuid":"579032362","full_name":"maximgurin/verifica","owner":"maximgurin","description":"Verifica is Ruby's most scalable authorization solution","archived":false,"fork":false,"pushed_at":"2023-10-25T17:03:26.000Z","size":111,"stargazers_count":61,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-11-07T03:15:40.680Z","etag":null,"topics":["acl","authorization","ruby"],"latest_commit_sha":null,"homepage":"https://verifica-rails-example.maximgurin.com","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/maximgurin.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null}},"created_at":"2022-12-16T13:44:38.000Z","updated_at":"2024-09-13T10:19:32.000Z","dependencies_parsed_at":"2023-02-12T01:47:28.290Z","dependency_job_id":"d72d43ff-0bfa-4bf8-ac88-08babcd28826","html_url":"https://github.com/maximgurin/verifica","commit_stats":{"total_commits":42,"total_committers":3,"mean_commits":14.0,"dds":0.04761904761904767,"last_synced_commit":"aec7085a879a6c0295cb13662d07a79ac89159d1"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maximgurin%2Fverifica","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maximgurin%2Fverifica/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maximgurin%2Fverifica/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maximgurin%2Fverifica/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maximgurin","download_url":"https://codeload.github.com/maximgurin/verifica/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226138849,"owners_count":17579496,"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":["acl","authorization","ruby"],"created_at":"2024-08-06T08:01:45.147Z","updated_at":"2024-11-24T07:30:55.844Z","avatar_url":"https://github.com/maximgurin.png","language":"Ruby","readme":"[![Gem Version](https://badge.fury.io/rb/verifica.svg)](https://badge.fury.io/rb/verifica)\n[![CI](https://github.com/maximgurin/verifica/actions/workflows/ci.yml/badge.svg)](https://github.com/maximgurin/verifica/actions/workflows/ci.yml)\n[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/maximgurin/verifica)\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/457e56b0bb514539844a94d85abe99f9)](https://www.codacy.com/gh/maximgurin/verifica/dashboard?utm_source=github.com\u0026amp;utm_medium=referral\u0026amp;utm_content=maximgurin/verifica\u0026amp;utm_campaign=Badge_Grade)\n[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/457e56b0bb514539844a94d85abe99f9)](https://www.codacy.com/gh/maximgurin/verifica/dashboard?utm_source=github.com\u0026utm_medium=referral\u0026utm_content=maximgurin/verifica\u0026utm_campaign=Badge_Coverage)\n![GitHub](https://img.shields.io/github/license/maximgurin/verifica)\n\n# Verifica\n\nVerifica is Ruby's most scalable authorization solution ready to handle sophisticated authorization rules.\n\n- Framework and database agnostic\n- Scalable. Start from 10, grow to 10M records in the database while having the same authorization architecture\n- Supports any actor in your application. Traditional `current_user`, external service, API client, you name it\n- No global state. Only local, immutable objects\n- Plain old Ruby, zero dependencies, no magic\n\nVerifica is designed around Access Control List. ACL powers a straightforward and unified authorization flow\nfor any user and resource, regardless of how complex the authorization rules are.\n\n*Note: Verifica is a new open-source gem, so you may wonder if it's reliable. Internally,\nthis solution has been battle-tested in several B2B products, including one with over 15M database records.\nBut DYOR anyway.*\n\n## Why Verifica? Isn't Pundit or CanCanCan enough?\n\nLet's say you are working on a video platform application:\n\n- You have 10M videos in the database\n- 7 types of user roles\n- 20 rules defining who is allowed to access the video\n- Rules require querying other entities too (video author settings, author's organization settings, etc.)\n\nGiven all these, *how do you even find a list of videos available for `current_user`?*\nBunch of `if/elsif` and enormous SQL query with many joins? Is there a better way? Verifica shines for this kind of problem.\nIn the [Real-world example with Rails](#real-world-example-with-rails) you can see the solution in detail.\n\n## Basic example\n\n```ruby\nrequire \"verifica\"\n\nUser = Struct.new(:id, :role, keyword_init: true) do\n  # Verifica expects each security subject to respond to #subject_id, #subject_type, and #subject_sids\n  alias_method :subject_id, :id\n  def subject_type = :user\n\n  def subject_sids(**)\n    role == \"root\" ? [\"root\"] : [\"authenticated\", \"user:#{id}\"]\n  end\nend\n\nVideo = Struct.new(:id, :author_id, :public, keyword_init: true) do\n  # Verifica expects each secured resource to respond to #resource_id, and #resource_type\n  alias_method :resource_id, :id\n  def resource_type = :video\nend\n\nvideo_acl_provider = lambda do |video, **|\n  Verifica::Acl.build do |acl|\n    acl.allow \"root\", [:read, :write, :delete, :comment]\n    acl.allow \"user:#{video.author_id}\", [:read, :write, :delete, :comment]\n\n    if video.public\n      acl.allow \"authenticated\", [:read, :comment]\n    end\n  end\nend\n\nauthorizer = Verifica.authorizer do |config|\n  config.register_resource :video, [:read, :write, :delete, :comment], video_acl_provider\nend\n\npublic_video = Video.new(id: 1, author_id: 1000, public: true)\nprivate_video = Video.new(id: 2, author_id: 1000, public: false)\n\nsuperuser = User.new(id: 777, role: \"root\")\nvideo_author = User.new(id: 1000, role: \"user\")\nother_user = User.new(id: 2000, role: \"user\")\n\nauthorizer.authorized?(superuser, private_video, :delete) # =\u003e true\nauthorizer.authorized?(video_author, private_video, :delete) # =\u003e true\nauthorizer.authorized?(other_user, private_video, :read) # =\u003e false\nauthorizer.authorized?(other_user, public_video, :comment) # =\u003e true\n\nbegin\n  # raises Verifica::AuthorizationError: Authorization FAILURE. Subject 'user' id='2000'. Resource 'video' id='1'. Action 'write'\n  authorizer.authorize(other_user, public_video, :write)\nrescue Verifica::AuthorizationError =\u003e e\n  e.explain # =\u003e Long-form explanation of why action is not authorized, your debugging friend\nend\n\n# #authorization_result returns a special object with a bunch of useful info\nauth_result = authorizer.authorization_result(superuser, private_video, :delete)\nauth_result.success? # =\u003e true\nauth_result.subject_id # =\u003e 777\nauth_result.resource_type # =\u003e :video\nauth_result.action # =\u003e :delete\nauth_result.allowed_actions # =\u003e [:read, :write, :delete, :comment]\nauth_result.explain # =\u003e Long-form explanation of why action is authorized\n```\n\n## Installation\n\n**Required Ruby version \u003e= 3.0**\n\nInstall the gem and add to the application's Gemfile by executing:\n\n```bash\n$ bundle add verifica\n```\n\n## Core concepts\n\nGet a high-level overview of Verifica's core concepts and architecture before diving into usage nuances.\nVerifica may appear complex initially, but it prioritizes explicitness, flexibility, and scalability over nice looking magic.\nHere is an explanation of each component:\n\n### Subject\n\nSecurity subject is a user, process, or system granted access to specific resources.\nIn most applications the subject is currently authenticated user, aka `current_user`.\n\nIn code a subject could be represented by any object that responds to `#subject_id`, `#subject_type`, and `#subject_sids`.\n\n```ruby\nclass User\n  def subject_id\n    123\n  end\n\n  def subject_type\n    :user\n  end\n\n  def subject_sids\n    [\"root\"] # see Security Identifier section below to understand what is this for\n  end\nend\n```\n\n### Resource\n\nResource refers to anything that requires protection.\nIn most applications resources are entities stored in the database, such as Post, Comment, User, etc.\n\nIn code a resource could be represented by any object that responds to `#resource_id` and `#resource_type`.\n\n```ruby\nclass Post\n  def resource_id\n    1\n  end\n\n  def resource_type\n    :post\n  end\nend\n```\n\n### Action\n\nAction that Subject can perform on a protected Resource. Represented as a Symbol in code,\nit could be traditional `:read`, `:write`, `:delete` or more domain specific `:comment`, `:publish`, etc.\n\n### Security Identifier\n\nSID is a value used to identify and differentiate Subjects\nand assign access rights based on the subject's attributes like role, organization, group, or country.\n\nIn code SID could be represented by immutable string (other objects work too, equality check is the only requirement).\nEach subject has one or more SIDs.\n\n```ruby\nsuperuser.subject_sids         # =\u003e [\"root\"]\nmoderator_user.subject_sids    # =\u003e [\"user:321\", \"role:moderator\"]\nregular_user.subject_sids      # =\u003e [\"authenticated\", \"user:123\", \"country:UA\"]\norganization_user.subject_sids # =\u003e [\"authenticated\", \"user:456\", \"country:UA\", \"org:789\"]\nanonymous_user.subject_sids    # =\u003e [\"anonymous\", \"country:UA\"]\n```\n\n### Access Control List\n\nACL consists of Access Control Entries (ACEs) and defines which actions are allowed or denied for particular SIDs.\nACL is associated with a specific protected resource in your system.\n\n```ruby\nvideo_acl = Verifica::Acl.build do |acl|\n  acl.allow \"authenticated\", [:read, :comment]\n  acl.deny \"country:US\", [:read]\nend\n\nvideo_acl.to_a\n# =\u003e\n# [#\u003cVerifica::Ace:0x00007fab1955dd60 @action=:read, @allow=true, @sid=\"authenticated\"\u003e,\n#  #\u003cVerifica::Ace:0x00007fab1955dd10 @action=:comment, @allow=true, @sid=\"authenticated\"\u003e,\n#  #\u003cVerifica::Ace:0x00007fab1955dc48 @action=:read, @allow=false, @sid=\"country:US\"\u003e]\n```\n\n### AclProvider\n\nAclProvider is an object that responds to `#call(resource, **)` and returns ACL for the given resource.\n\n```ruby\nclass VideoAclProvider\n  def call(video, **context)\n    Verifica::Acl.build do |acl|\n      acl.allow \"user:#{video.author_id}\", [:read, :write, :delete, :comment]\n\n      if video.public?\n        acl.allow \"authenticated\", [:read, :comment]\n      end\n    end\n  end\nend\n```\n\n### Authorizer\n\nAnd finally, Authorizer, the heart of Verifica. It couples all concepts above into an isolated container with no global state.\nEach Authorizer has a list of resource types registered with their companion AclProviders and\nseveral methods to check the Subject's rights to perform a specific action on a given resource.\n\nCheck the [Basic example](#basic-example) above to see how it all plays together.\n\n## Real-world example with Rails\n\nDemo: https://verifica-rails-example.maximgurin.com\n\nLet's say you started working on your *next big thing* idea — a video hosting application.\nIn the beginning, you have only 2 user types and straightforward rules:\n\n- *Admins* can see all videos\n- *Users* can see their own videos and public videos of other users\n\n```ruby\nclass Video\n  scope :available_for, -\u003e(user) do\n    where(public: true).or(where(author_id: user.id)) unless user.admin?\n  end\nend\n\nclass VideosController\n  def index\n    @videos = Video.available_for(current_user)\n  end\nend\n```\n\nTime goes by and 4 years later you have:\n\n- 10M records in the videos table. Organization and personal user accounts\n- 4 roles: *Admin*, *Moderator*, *Organization Admin*, *User*\n- Video drafts available only to their authors\n- Internal videos available only for members of the author's organization\n- Country restrictions, either in the *allowlist* or *denylist* modes\n- Distribution Settings entity with one-to-many relation to Videos\n  - Distribution mode: *public*, *internal*, or *private*\n  - Countries *allowlist* or *denylist*\n- Organization-wide country restrictions overrides Distribution Settings\n- *Organization Admins* can see private videos of their org members\n- *Admins* and *Moderators* can see all videos, regardless of country restrictions\n\nWow, that's a pretty extensive list of requirements. Easy to get lost!\nNow the most exciting part. How do you implement `Video.available_for` method with so many details to consider?\nVideos table is big, so you can't use SQL joins to, let's say, check the video author's organization or other dependencies.\nAnd even if you can, a query with so many joins and conditions would be write-only anyway :)\n\nHere is how this challenge could be resolved using Verifica and ACL:\n\n```ruby\n# app/acl_providers/video_acl_provider.rb\n\nclass VideoAclProvider\n  include Verifica::Sid\n\n  POSSIBLE_ACTIONS = [:read, :write, :delete].freeze\n\n  def call(video, **)\n    Verifica::Acl.build do |acl|\n      acl.allow root_sid, POSSIBLE_ACTIONS\n      acl.allow user_sid(video.author_id), POSSIBLE_ACTIONS\n      acl.allow role_sid(\"moderator\"), [:read, :delete]\n\n      next if video.draft?\n\n      ds = video.distribution_setting\n      author_org = video.author.organization\n      allowed_countries = author_org\u0026.allow_countries || ds.allow_countries\n      denied_countries = author_org\u0026.deny_countries || ds.deny_countries\n\n      # ...and 30 more lines to handle all our requirements\n    end\n  end\nend\n```\n\n```ruby\n# config/initializers/verifica.rb\n\nrequire \"verifica\"\n\n# Quick and dirty way for simplicity\n# In the real app, you could use DI container to hold configured Verifica::Authorizer instance\nRails.configuration.after_initialize do\n  AUTHORIZER = Verifica.authorizer do |config|\n    config.register_resource :video, VideoAclProvider::POSSIBLE_ACTIONS, VideoAclProvider.new\n  end\nend\n```\n\n```ruby\n# app/models/user.rb\n\nclass User \u003c ApplicationRecord\n  include Verifica::Sid\n\n  alias_method :subject_id, :id\n\n  def subject_type = :user\n\n  def subject_sids(**)\n    case role\n    when \"root\"\n      [root_sid]\n    when \"moderator\"\n      [user_sid(id), role_sid(\"moderator\")]\n    when \"user\"\n      sids = [authenticated_sid, user_sid(id), country_sid(country)]\n      organization_id.try { |org_id| sids.push(organization_sid(org_id)) }\n      sids\n    when \"organization_admin\"\n      sids = [authenticated_sid, user_sid(id), country_sid(country)]\n      sids.push(organization_sid(organization_id))\n      sids.push(role_sid(\"organization_admin:#{organization_id}\"))\n    else\n      throw RuntimeError(\"Unsupported user role: #{role}\")\n    end\n  end\nend\n```\n\nWhat we've done:\n\n- Configured `Verifica::Authorizer` object. It's available as `AUTHORIZER` constant anywhere in the app\n- Registered `:video` type as a secured resource. `VideoAclProvider` defines rules, who can do what\n- Configured `User` to be a security Subject. Each user has list of Security Identifiers depending on the role and other attributes\n\nNow, a few last steps and the challenge resolved:\n\n```ruby\n# db/migrate/20230113203815_add_read_sids_to_videos.rb\n\n# For simplicity, we are adding two String array columns directly to videos table.\n# In the real app, you could use something like ElasticSearch to hold videos with these companion columns\nclass AddReadSidsToVideos \u003c ActiveRecord::Migration[7.0]\n  def change\n    add_column :videos, :read_allow_sids, :string, null: false, array: true, default: [], index: true\n    add_column :videos, :read_deny_sids, :string, null: false, array: true, default: [], index: true\n  end\nend\n```\n\n```ruby\n# app/models/video.rb\n\nclass Video \u003c ApplicationRecord\n  attr_accessor :allowed_actions\n  alias_method :resource_id, :id\n\n  before_save :update_read_acl\n\n  def resource_type = :video\n\n  def update_read_acl\n    acl = AUTHORIZER.resource_acl(self)\n    self.read_allow_sids = acl.allowed_sids(:read)\n    self.read_deny_sids = acl.denied_sids(:read)\n  end\n\n  # And finally, this is our goal. Straightforward implementation regardless of how complex the rules are.\n  scope :available_for, -\u003e(user) do\n    sids = user.subject_sids\n    where(\"read_allow_sids \u0026\u0026 ARRAY[?]::varchar[]\", sids).where.not(\"read_deny_sids \u0026\u0026 ARRAY[?]::varchar[]\", sids)\n  end\nend\n```\n\n```ruby\n# app/controllers/videos_controller\n\nclass VideosController\n  def index\n    @videos = Video\n      .includes(:distribution_setting, author: [:organization])\n      .available_for(current_user)\n      .order(:name)\n      .limit(50)\n  end\n\n  def show\n    @video = Video.find(params[:id])\n\n    # upon successful authorization helper object is returned with a bunch of useful info\n    auth_result = AUTHORIZER.authorize(current_user, @video, :read)\n\n    # add list of allowed actions so the frontend knows whether show \"Edit\" and \"Delete\" buttons, for example\n    @video.allowed_actions = auth_result.allowed_actions\n  end\n\n  def destroy\n    video = Video.find(params[:id])\n    AUTHORIZER.authorize(current_user, video, :delete)\n    video.destroy\n  end\nend\n```\n\nVoila, we're done! So now, no matter how sophisticated our authorization rules are,\nwe have a clear method to find available videos for any user.\nNo conditions, no special handling for superusers as everyone goes through the unified mechanism.\n\n**Important points not covered in this example but needed in the real app**:\n\n- **Dependency change handling.** If country restrictions changed on the organization level you need to\nrun a background job to find all affected videos and update `read_allow_sids`, `read_deny_sids` columns.\nSame applies to Distribution Settings and other dependencies.\n- **Rules change handling.** If implementation of `VideoAclProvider` changed you need to run a background job\nto update `read_allow_sids`, `read_deny_sids` columns for all videos.\n\nSee also:\n\n- Live demo - https://verifica-rails-example.maximgurin.com\n- Full source code - https://github.com/maximgurin/verifica-rails-example\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.\nYou 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`.\nTo release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`,\nwhich will create a git tag for the version, push git commits and the created tag,\nand 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/maximgurin/verifica.\nThis project is intended to be a safe, welcoming space for collaboration, and contributors are expected\nto adhere to the [code of conduct](https://github.com/maximgurin/verifica/blob/master/CODE_OF_CONDUCT.md).\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone interacting in the Verifica project's codebases, issue trackers, chat rooms and mailing lists is\nexpected to follow the [code of conduct](https://github.com/maximgurin/verifica/blob/master/CODE_OF_CONDUCT.md).\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaximgurin%2Fverifica","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaximgurin%2Fverifica","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaximgurin%2Fverifica/lists"}