1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
|
# `DeclarativePolicy`: A Declarative Authorization Library
[](https://badge.fury.io/rb/declarative_policy)
This library provides a DSL for writing authorization policies.
It can be used to separate logic from permissions, and has been
used at scale in production at [GitLab.com](https://gitlab.com).
The original author of this library is [Jeanine Adkisson](http://jneen.net),
and copyright is held by GitLab.
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'declarative_policy'
```
And then execute:
```plain
$ bundle install
```
Or install it yourself as:
```plain
$ gem install declarative_policy
```
## Example
```ruby
require 'declarative_policy'
class User
attr_reader :name
def initialize(name:)
@name = name
end
end
class Vehicle
def initialize(owner:, trusted: [])
@owner = owner
@trusted = trusted
end
def owner?(user)
@owner.name == user.name
end
def trusted?(user)
@owner.name == user.name || @trusted.detect { |t| t.name == user.name }
end
end
class VehiclePolicy < DeclarativePolicy::Base
condition(:owns) { @subject.owner?(@user) }
condition(:trusted) { @subject.trusted?(@user) }
rule { owns }.enable :sell_vehicle
rule { trusted }.enable :drive_vehicle
end
jack = User.new(name: 'jack')
jill = User.new(name: 'jill')
jacks_vehicle = Vehicle.new(owner: jack, trusted: [jill])
jills_vehicle = Vehicle.new(owner: jill, trusted: [jack])
puts "Jack can drive Jack's vehicle? -> #{DeclarativePolicy.policy_for(jack, jacks_vehicle).can?(:drive_vehicle)}"
puts "Jack can drive Jill's vehicle? -> #{DeclarativePolicy.policy_for(jack, jills_vehicle).can?(:drive_vehicle)}"
puts "Jack can sell Jack's vehicle? -> #{DeclarativePolicy.policy_for(jack, jacks_vehicle).can?(:sell_vehicle)}"
puts "Jack can sell Jill's vehicle? -> #{DeclarativePolicy.policy_for(jack, jills_vehicle).can?(:sell_vehicle)}"
```
```plain
$ ruby example.rb
Jack can drive Jack's vehicle? -> true
Jack can drive Jill's vehicle? -> true
Jack can sell Jack's vehicle? -> true
Jack can sell Jill's vehicle? -> false
```
## Usage
The core abstraction of this library is a `Policy`. Policies combine:
- **facts** (called `conditions`) about the state of the world
- **judgements** about these facts (called `rules`)
This library exists to determine the truth value of statements of the form:
```plain
User Predicate [Subject]
```
Renaming `User` to `Actor` and `Subject` to `Resource` is discussed in
[this issue](https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/issues/6).
For example:
- `user :is_alive`
- `user :can_drive car`
- `user :can_sell car`
It does this by letting us associate a `Policy` (a set of rules about which
statements are true) with the objects of the sentences. A statement is
considered to hold if no rule `prevents` it, and at least one rule `enables` it.
For example, imagine we have a data model containing vehicles and users, and we
want to know if a user can drive a vehicle. We need a `VehiclePolicy`:
```ruby
class VehiclePolicy < DeclarativePolicy::Base
# relevant facts
condition(:owns) { @subject.owner == @user }
condition(:has_access_to) { @subject.owner.trusts?(@user) }
condition(:old_enough_to_drive) { @user.age >= laws.minimum_age }
condition(:has_driving_license) { @user.driving_license&.valid? }
# expensive rules can have 'score'. Higher scores are 'more expensive' to calculate
condition(:owns, score: 0) { @subject.owner == @user }
condition(:has_access_to, score: 3) { @subject.owner.trusts?(@user) }
condition(:intoxicated, score: 5) { @user.blood_alcohol > laws.max_blood_alcohol }
# conclusions we can draw:
rule { owns }.enable :drive_vehicle
rule { has_access_to }.enable :drive_vehicle
rule { ~old_enough_to_drive }.prevent :drive_vehicle
rule { intoxicated }.prevent :drive_vehicle
rule { ~has_driving_license }.prevent :drive_vehicle
# we can use methods to abstract common logic
def laws
@subject.registration.country.driving_laws
end
end
```
A few points to note: we could have written this as one big rule
(`(owns | has_access_to) & old_enough_to_drive & ~intoxicated & has_driving_license`)
but we can see some of the features that make declarative policies scalable for
large systems: rules can be broken up into small elements, and composed into
larger rules. New conditions and rules can be added at any time.
What is more difficult to see is that many performance optimizations are handled
for us transparently:
- more expensive conditions are called later
- we automatically get the desired groupings (evaluate all conditions that might
prevent an action, but stop once we have at least one call to enable).
- intermediate values are cached.
- policies support inheritance and delegation, meaning authorization logic
remains DRY.
In short this library aims to be declarative: we declare the rules that are
important, and the library arranges how to evaluate them.
Caching is a particularly valuable feature of policies. If we add new rules
about selling a vehicle, for example:
```ruby
rule { owns }.enable :sell_vehicle
```
Then the fact of ownership can be shared between different calls to the policy,
saving database calls and other expensive IO operations.
### Evaluating a policy:
We can check the determination of a policy with:
```ruby
cache = Session.current_session
policy = DeclarativePolicy.policy_for(user, car, cache: cache)
policy.can?(:drive_vehicle)
```
For more usage details, see the [documentation](doc).
## Development
After checking out the repository, run `bundle install` 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.
To 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).
## Additional Reading Material
More details on policies and custom roles can be found in the following pages:
- [Development Process for the DeclarativePolicy framework](https://docs.gitlab.com/ee/development/policies.html)
- [Custom Roles docs](https://docs.gitlab.com/ee/development/permissions/custom_roles.html)
## Contributing
Bug reports and merge requests are welcome on GitLab at
https://gitlab.com/gitlab-org/ruby/gems/declarative-policy. This project is intended to be
a safe, welcoming space for collaboration, and contributors are expected to
adhere to the [GitLab code of conduct](https://about.gitlab.com/community/contribute/code-of-conduct/).
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
## Code of Conduct
Everyone interacting in the `DeclarativePolicy` project's codebase, issue
trackers, chat rooms and mailing lists is expected to follow
the [code of conduct](https://github.com/[USERNAME]/declarative-policy/blob/master/CODE_OF_CONDUCT.md).
|