hyperstack-org / hyperstack-org/hyperstack
document regulate_scope
- Dominant language
- JavaScript
- Stars
- 538
- Forks
- 41
- PR merge metrics
- No merged PRs in 30d
Description
For now here is a very brief summary:
Scope regulation prevents the unauthorized return of any aggregate data (such as `count`) for a scope.
Without scope regulation a hacker could perform the following operation on the database without having any access permission:
`Orders.all.count`
For the purpose of regulation `has_many` relations are considered specialized scopes.
> Note that returning any specific data from *individual* records is controlled by the rest of the policy mechanism. Regulating aggregate data is a special case since it does not pertain to any specific record, but some aggregate property of all the records.
**Chaining Scopes and Relations**
Scopes can be chained and combined with relations, thus we might say:
`some_user.orders.out_for_delivery`
or
`Order.out_for_delivery`
When scopes are chained they always *restrict* the amount of data being returned. That is adding a scope to a chain of scopes will *never* increase the amount of data being returned. **This is key to understanding how scope regulations work**.
Scope regulations take advantage of the above fact by returning in essence one of three values:
+ *Okay* - represented by any truthy value
+ *Dont Know* - represented by any falsy value)
+ *Access Denied* - represented by the special `denied!` method
Access to the scope chain is allowed as long as at least one scope's regulation says its okay (i.e. returns a truthy value) **and** no scope in the chain denies access. This (almost) always works because if one scope in the chain said its okay, then the other scopes are doing nothing but further reducing the amount of data returned. There are some very special cases, and the `denied!` method can be applied to cover those.
Thus we might allow a user to access to their own `orders` relation, and allow access to the `out_for_delivery` scope **if** the current acting user is an admin, otherwise we don't know if ultimate scope will have access.
Thus `Order.out_for_delivery` will fail unless the acting_user is an admin, but
`some_user.orders.out_for_delivery` will succeed as long as the current acting_user is the same as `some_user`.
Scope regulation can be specified in a couple of ways:
**Separate Regulations: `regulate_scope` `regulate_relationship` and `regulate_default_scope`**
These methods are added to `ActiveRecord::Base`, and have the following form:
+ `regulate_scope name_of_scope, regulation-or-block`
+ `regulate_relationship name_of_relationship, regulation-or-block` and
+ `regulate_default_scope regulation-or-block`
Where `name_of_scope` (or name_of_relationship) is the symbol naming the scope (or relationship), and `regulation-or-block` has the following forms:
+ a proc (or just a block) which will return true/false value or call the `denied!` method. Inside the proc the method `acting_user` will return the current acting user.
+ the symbols `:denied!`,`:deny`, or `:denied`, which is the same as unconditionally invoking the `denied!` method inside a proc.
+ nil or false, which is effectively a nop, or
+ any other value, which is saying that this scope is unconditionally allowed. By convention we use the symbol `:always_allow`, but any value will do.
So for example we might say:
```ruby
class Order < ApplicationRecord
regulate_scope :out_for_delivery, ->() { acting_user&.admin? }
# or even just
regulate_scope(:out_for_delivery) { acting_user&.admin? }
end
class User < ApplicationRecord
regulate_relationship :orders, -> () { self == acting_user }
end
```
In a typical application an adminstrator might be able to query any relationship or scope
so we could say this:
```ruby
class ApplicationRecord < ActiveRecord::Base
regulate_scope :all, ->() { acting_user&.admin? }
end
```
and because `all` is implicitly part of every scope chain, this will allow admins to make any query unless there is an explicity denial someplace in the query chain.
Likewise during development you might say:
```ruby
class ApplicationRecord < ActiveRecord::Base
regulate_scope :all, :always_allow
# or even better
regulate_scope :all, !Rails.env.production?
end
```
**Embedding regulations in scope and relationship declarations**
You can also directly add regulations to scopes and relationships:
```ruby
class User < ApplicationRecord
has_many :orders, regulate: -> () { acting_user == self }
end
class Order < ApplicationRecord
scope :out_for_delivery,
->() { where(state: 'out-for-delivery') },
regulate: -> () { acting_user&.admin? }
end
```
**server and finder methods**
Hyperstack adds the server_method and find_method macros to active record, which create methods that can be invoked from the client, but always run on the server. These methods are "self regulating" in the sense that the methods themselves will call `denied!` if access to the method should be denied, and they also have access to the `acting_user` method to facilitate the evaluation. For example:
```ruby
class Order < ApplicationRecord
server_method :internal_wholesale_price do
denied! unless acting_user.admin?
# compute and return the wholesale price...
end
end
```
**A word regarding paranoia**
Keep in mind, again that scope regulations are just protecting against authorized aggregation of data. In reality do you really care if somebody found out how many orders you had out for shipment? Don't get too carried away!
Contributor guide
Assessment
This issue has not been assessed yet.