Hyerarchical scopes definition and validation for Bolder apps and APIs.
gem 'bolder_scopes'
Scopes are permission trees.
For example, the scope all.users.create
represents the following structure:
all
users
create
A request's token scope is compared with a given endpoint's token to check access permissions, from left to right.
all
has access toall
all
has access toall.users
all.users
has access toall.users.create
all.accounts
does NOT have access toall.users
all.accounts
does NOT have access toall.users.create
Wildcard scopes are possible using the special character *
as one or more segments in a scope.
For example:
all.*.create
has access toall.accounts.create
orall.photos.create
all.*.create
has access toall.accounts.*
all.*.create.*
does not have access toall.accounts.create
(because it's more specific).
def index
request_scope = access_token.scope # ex. bolder.accounts.123.shops.*.read
resource_scope = Bolder::Scopes.wrap(['bolder', 'accounts', current_account.id, 'shops', 'read'].join('.'))
if request_scope >= resource_scope
render
else
render :unauthorized, status: 403
end
end
Defining scopes as strings can be error prone (easy to make typos or get the hierarchy wrong!).
The Bolder::Scopes::Tree
utility can be helpful to define all possible scope hierarchies in a single place.
SCOPES = Bolder::Scopes::Tree.new('all') do |all|
all.users.update
all.users.read
all.users.create
all.orders.read
end
The scope tree will expose all defined hierarchies
SCOPES.all.users # 'all.users'
SCOPES.all.users.create # 'all.users.create'
... But not invalid hierarchies.
SCOPES.all.users.orders # => raises Bolder::Scopes::Scope::InvalidScopHierarchyError
Wilcards work too
SCOPES.all.*.read # 'all.*.read`
Note that wildcards only allow sub-scopes that are shared by all children.
SCOPES.all.*.read # Ok
SCOPES.all.*.update # raises InvalidScopHierarchyError because not all children of `all.*` support `update`
Hierarchies can also be defined using the > operator: This can help avoid typos.
SCOPES = Bolder::Scopes::Tree.new('bolder') do |bolder|
api = 'api'
products = 'products'
orders = 'orders'
own = 'own'
all = 'all'
read = 'read'
bolder > api > products > own > read
bolder > api > products > all > read
bolder > api > orders > own > read
end
Block notation can be used where it makes sense:
SCOPES = Bolder::Scopes::Tree.new('bolder') do |bolder|
bolder.api.products do |n|
n.own do |n|
n.read
n.write
n > 'list' # use `>` to append variables or constants
end
end
end
Block notation also works without explicit node argument (but can't access outer variables):
SCOPES = Bolder::Scopes::Tree.new('bolder') do
api.products do
own do
read
write
end
all do
read
end
end
end
Use _any
to define segments that can be anything:
SCOPES = Bolder::Scopes::Tree.new('bolder') do |bolder|
bolder.api.products._any.read
end
_any
takes an optional list of allowed values, in which case it has "any of" semantics.
Values are matched with #===
operator, so they can be regular expressions.
If no values are given, _any
has "anything" semantics.
_any
can be used to define a catch-all scope:
SCOPES = Bolder::Scopes::Tree.new('bolder') do |bolder|
bolder.api do |s|
s.products do |s|
s._any('my_products', /^\d+$/) do |s| # matches 'my_products' or any number-like string
s.read
end
end
end
With the above, the following scopes are allowed, using parenthesis notation to allow numbers and multiple values
bolder.api.products.(123).read # 'bolder.api.products.123.read'
bolder.api.products.(1, 2, 3).read # 'bolder.api.products.(1,2,3).read'
bolder.api.products.('my_products').read # 'bolder.api.products.my_products.read'
bolder.api.products.my_products.read # works too
Bolder::Scopes::Map
can be used to expand one scope to others.
map = Bolder::Scopes::Map.new('read' => ['read.users'])
map.map('read') # => ['read.users']
map.map('read:users') # => ['read.users']
Use Map#expand
to include the original scope in the resulting list.
map = Bolder::Scopes::Map.new('read' => 'read.users')
map.expand('read') # => Scopes['read', 'read.users']
Scope trees also work with scope maps.
map = Bolder::Scopes::Map.new(
SCOPES.admin => [SCOPES.api.products.own, SCOPES.api.orders.own, SCOPES.api.all.read],
'god' => [SCOPES.api]
)
After 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.
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 the created tag, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/bolder/bolder_scopes.