{"id":17656517,"url":"https://github.com/imdrasil/sage","last_synced_at":"2025-05-07T11:44:14.043Z","repository":{"id":81841575,"uuid":"132274522","full_name":"imdrasil/sage","owner":"imdrasil","description":"Minimal authorization library","archived":false,"fork":false,"pushed_at":"2018-05-09T12:48:02.000Z","size":8,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-21T08:14:05.418Z","etag":null,"topics":["authorization","crystal"],"latest_commit_sha":null,"homepage":"","language":"Crystal","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/imdrasil.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2018-05-05T18:33:40.000Z","updated_at":"2023-07-25T14:16:54.000Z","dependencies_parsed_at":null,"dependency_job_id":"6d04beb4-7abe-4a54-a9b3-8454e2a350c2","html_url":"https://github.com/imdrasil/sage","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imdrasil%2Fsage","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imdrasil%2Fsage/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imdrasil%2Fsage/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imdrasil%2Fsage/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/imdrasil","download_url":"https://codeload.github.com/imdrasil/sage/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252873907,"owners_count":21817708,"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","crystal"],"created_at":"2024-10-23T14:33:20.138Z","updated_at":"2025-05-07T11:44:14.036Z","avatar_url":"https://github.com/imdrasil.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sage\n\nSage - is a lightweight library for defining resource access policy rules.\n\n## Installation\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n  sage:\n    github: imdrasil/sage\n```\n\n## Usage\n\nThe core component of Sage is a *policy class* - it describes access policies to resource. That's why it is assumed you define a separate policy class for each resource you want to specify access restrictions.\n\nConsider a simple example:\n\n```crystal\n# It is not necessary to define application base policy class\n# but this allows to put all shared behavior and configs in one place\nabstract class ApplicationPolicy \u003c Sage::Base\nend\n\nclass PostPolicy \u003c ApplicationPolicy\n  constructor(User, Post)\n\n  ability :edit?\n    user.admin? || user.id == resource.id\n  end\n\n  ability :show?\n    true\n  end\nend\n```\n\nNow you can add authorization to your app:\n\n```crystal\nabstract class ApplicationController\n  include Sage::Behavior\n\n  private def current_user\n    User.current_user\n  end\nend\n\nclass PostsController\n  def update\n    @post = Post.find(params[\"id\"])\n    authorize! :update?, @post\n\n    # ...\n  end\nend\n```\n\nIn the above example Sage automatically refers policy class from the given `@post` variable - `Post -\u003e PostPolicy`. The `user` is automatically used from calling `sage_user` method (which by default calls `current_user`).\n\nWhen authorization is passed successfully (corresponding ability returned `true`), nothing happens, but in case of an authorization failure `Sage::UnauthorizedError` error is raised.\n\nThere are also an `able?` nad `unable?` methods which return `true` or `false`:\n\n```crystal\nable?(:update?, @post)\nunable?(:update?, @post)\n```\n\nAlso you may specify exact policy class:\n\n```crystal\nable?(:update, @post, within: EditorPostPolicy)\nauthorize!(:update?, @post, within: EditorPostPolicy)\n```\n\n### Writing Policies\n\nPolicy class contains defined abilities (partially they are just a predicate methods) which are used to authorize activities.\n\nEach policy record is instantiated with the target `resource : T` object and authorization context `user : U`. To avoid generics, they should define corresponding attribute types for themselves. As a plugin `constructor` macro could be used for doing this:\n\n```crystal\nclass PostPolicy \u003c Sage::Base\n  constructor(User, Post)\n\n  # This call is the same as\n\n  getter user : User, resource : Post\n\n  def initialize(@user, @resource)\n  end\nend\n```\n\n\u003e NOTE: `#user` method is abstract so should be defined by subclasses.\n\nTo define ability use corresponding macro `ability`:\n\n```crystal\nclass PostPolicy \u003c Sage::Base\n  # ...\n  ability :update? do\n    user.admin? || user.id == resource.user_id\n  end\nend\n```\n\n#### Calling other policies\n\nIt may be useful to call other resource policy from within a current one. For doing this you can use standard `#able?` and `#unable?` methods:\n\n```crystal\nclass CommentPolicy \u003c Sage::Policy\n  # ...\n\n  ability :update? do\n    user.admin? || user.id == resource.id || able?(:update?, resource.post)\n  end\nend\n```\n\n### Testing\n\nPolicies can be tested as any other Crystal classes:\n\n```crystal\ndescribe PostPolicy do\n  described_class = PostPolicy\n\n  describe \"#update?\"\n    it \"returns false when the user is not admin nor author\" do\n      user = User.new\n      post = Post.new\n      policy = described_class.new(user, post)\n      policy.apply(:update?).should be_false\n    end\n\n    it \"returns true when the user is admin\" do\n      user = User.new(:admin)\n      post = Post.new\n      policy = described_class.new(user, post)\n      policy.apply(:update?).should be_true\n    end\n\n    it \"returns true when the user is author\" do\n      user = User.new\n      post = Post.new(user_id: user.id)\n      policy = described_class.new(user, post)\n      policy.apply(:update?).should be_true\n    end\n  end\nend\n```\n\n### Aliases\n\nSage allows you to add ability aliases. It may be useful when you rely on implicit rules in your code:\n\n```crystal\nclass PostController\n  def edit\n    # ...\n    authorize! :edit?, @post\n    # ...\n  end\n\n  def update\n    # ...\n    authorize! :update?, @post\n    # ...\n  end\n\n  def destroy\n    # ...\n    authorize! :destroy?, @post\n    # ...\n  end\nend\n```\n\nIn your policy you can create alias to avoid code duplication:\n\n```crystal\nclass PostPolicy \u003c Sage::Base\n  # ...\n  alias_ability :update?, :edit?, to: :update?\n  # ...\nend\n```\n\n\u003e NOTE: `alias_ability` doesn't create aliased methods and resolve them only during `Sage::Base#apply` call (which is under the hood of `able?` and `authorize!`).\n\n#### Default Ability\n\nWhen Sage can't resolve ability name it calls `Sage::Base#default_ability` method which by default returns `false`. You may override it to define another behavior.\n\n### Pre-Checks\n\nSometimes it happens that some of your abilities (or even all of them) starts with the same conditions. Example:\n\n```crystal\nclass PostPolicy \u003c Sage::Base\n  # ...\n  ability :show? do\n    user.admin? || resource.published?\n  end\n\n  ability :update? do\n    user.admin? || user.id == resource.user_id\n  end\n  # ...\nend\n```\n\nYou can separate the common parts from all abilities to a separate *pre-checks*:\n\n```crystal\nclass PostPolicy \u003c Sage::Base\n  # ...\n  pre_check :admin?\n\n  ability :show? do\n    resource.published?\n  end\n\n  ability :update? do\n    user.id == resource.user_id\n  end\n\n  private def admin?\n    allow! if user.admin?\n  end\n  # ...\nend\n```\n\nPre-checks are executed before ability invocation. They allow to halt the authorization process - just return `allow!` or `disallow!` call value. Any other returned value is ignored.\n\n## Contributing\n\n1. Fork it ( https://github.com/imdrasil/sage/fork )\n2. Create your feature branch (git checkout -b my-new-feature)\n3. Commit your changes (git commit -am 'Add some feature')\n4. Push to the branch (git push origin my-new-feature)\n5. Create a new Pull Request\n\n## Contributors\n\n- [imdrasil](https://github.com/imdrasil) Roman Kalnytskyi - creator, maintainer\n\n### Inspired by\n\n- [Action Policy](https://github.com/palkan/action_policy)\n- [Pundit](https://github.com/varvet/pundit)\n- [CancanCan](https://github.com/CanCanCommunity/cancancan)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fimdrasil%2Fsage","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fimdrasil%2Fsage","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fimdrasil%2Fsage/lists"}