Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature custom grant types #37

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ a_grant.to_s
"baz/bar".to_mumukit_grant.allows? Mumukit::Auth::Slug.join('foo') # false
```

#### Defining custom Grants

Grants can be extend, by inheriting from `Mumukit::Auth::Grant::Base`, and defining the following method:

* `#allows?(resource_slug)`: mandatory
* `#to_s`: mandatory
* `#granted_organizations`: optional
* `.try_parse(pattern)`: mandatory

New grant types must be registered using: `Mumukit::Auth::Grant.add_custom_grant_type!`

### Roles

```ruby
Expand Down Expand Up @@ -136,7 +147,7 @@ some_permissions.remove_permission! :student, 'foo/bar'
some_permissions.update_permission! :student, 'foo/*', 'foo/bar'

# Checking permissions
some_permissions.has_permission? :student, 'foo/_'
some_permissions.authorizes? :student, 'foo/_'
some_permissions.student? 'foo/_' # equivalent to previous line
some_permissions.protect! :student, 'foo/_' # similar to previous samples,
# but raises and exception instead
Expand Down
150 changes: 122 additions & 28 deletions lib/mumukit/auth/grant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,43 @@ def to_mumukit_grant
end
end

module Mumukit::Auth::Grant

# Parses a given string that describes a grant
# by trying with each grant type, in the following order:
#
# 1. All grant
# 2. Custom grants
# 3. First Grant
# 4. Slug grant
#
# Raises a `Mumukit::Auth::InvalidGrantFormatError` if the grant is not valid, which happens when
#
# * structure is invalid
# * `something/_`, `_/something`, `_/_` format is used
def self.parse(pattern)
grant_types.each do |type|
type.try_parse(pattern).try { |it| return it }
end
end

module Mumukit::Auth
class Grant
def self.grant_types
[AllGrant] + custom_grant_types + [FirstPartGrant, SingleGrant]
end

def self.add_custom_grant_type!(grant_type)
custom_grant_types << grant_type
end

def self.remove_custom_grant_type!(grant_type)
custom_grant_types.delete(grant_type)
end

def self.custom_grant_types
@custom_grant_types ||= []
end

class Base
def as_json(options={})
to_s
end
Expand All @@ -15,6 +49,14 @@ def to_mumukit_grant
self
end

# returns the organizations that are explicitly
# granted by this grant
#
# Custom grants may override this method
def granted_organizations
[]
end

def ==(other)
other.class == self.class && to_s == other.to_s
end
Expand All @@ -29,68 +71,120 @@ def inspect
"<Mumukit::Auth::Grant #{to_s}>"
end

def self.parse(pattern)
case pattern
when '*' then
AllGrant.new
when '*/*' then
AllGrant.new
when /(.*)\/\*/
FirstPartGrant.new($1)
else
SingleGrant.new(Slug.parse pattern)
end
# Tells wether the given grant
# is authorized by this grant
#
# This method exist in order to implement double dispatching
# for both grant and slugs authorization
#
# See:
# * `Mumukit::Auth::Slug#authorized_by?`
# * `Mumukit::Auth::Grant::Base#allows?
# * `Mumukit::Auth::Grant::Base#includes?`
def authorized_by?(grant)
grant.includes? self
end

# tells whether the given slug-like object is allowed by
# this grant
required :allows?

# tells whether the given grant-like object is included
# in - that is, is not broader than - this grant
#
# :warning: Custom grants **should not** override this method
def includes?(grant_like)
self == grant_like.to_mumukit_grant
end

# Returns a canonical string representation of this grant
# Equivalent grant **must** have equivalent string representations
required :to_s
end

class AllGrant < Grant
def allows?(_resource_slug)
class AllGrant < Base
def allows?(_slug_like)
true
end

def includes?(_)
Copy link
Contributor

@luchotc luchotc May 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_grant_like to make it more explicit?

true
end

def to_s
'*'
end

def to_mumukit_slug
Mumukit::Auth::Slug.new '*', '*'
def self.try_parse(pattern)
new if ['*', '*/*'].include? pattern
end
end

class FirstPartGrant < Grant
class FirstPartGrant < Base
attr_accessor :first

def initialize(first)
raise Mumukit::Auth::InvalidGrantFormatError, "Invalid first grant. First part must not be _" if first == '_'
@first = first.downcase
end

def allows?(resource_slug)
resource_slug.to_mumukit_slug.normalize!.match_first @first
def granted_organizations
[first]
end

def allows?(slug_like)
slug_like.to_mumukit_slug.normalize!.match_first @first
end

def to_s
"#{@first}/*"
end

def to_mumukit_slug
Mumukit::Auth::Slug.new @first, '*'
def includes?(grant_like)
grant = grant_like.to_mumukit_grant
case grant
when FirstPartGrant then grant.first == first
when SingleGrant then grant.slug.first == first
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could delegate first to slug in SingleGrant and compact this two lines like:

when FirstPartGrant, SingleGrant then grant.first == first

else false
end
end

def self.try_parse(pattern)
new($1) if pattern =~ /(.+)\/\*/
end
end

class SingleGrant < Grant
class SingleGrant < Base
attr_accessor :slug

def initialize(slug)
raise Mumukit::Auth::InvalidGrantFormatError, "Invalid slug grant. First part must not be _" if slug.first == '_'
raise Mumukit::Auth::InvalidGrantFormatError, "Invalid slug grant. Second part must not be _" if slug.second == '_'
@slug = slug.normalize
end

def allows?(resource_slug)
resource_slug = resource_slug.to_mumukit_slug.normalize!
resource_slug.match_first(@slug.first) && resource_slug.match_second(@slug.second)
def granted_organizations
[slug.first]
end

def allows?(slug_like)
slug = slug_like.to_mumukit_slug.normalize!
slug.match_first(@slug.first) && slug.match_second(@slug.second)
end

def to_s
@slug.to_s
end

def to_mumukit_slug
@slug
def self.try_parse(pattern)
new(Mumukit::Auth::Slug.parse pattern)
rescue Mumukit::Auth::InvalidSlugFormatError => e
raise Mumukit::Auth::InvalidGrantFormatError, "Invalid slug grant. Cause: #{e}"
end
end
end

module Mumukit::Auth
class InvalidGrantFormatError < StandardError
end
end
42 changes: 33 additions & 9 deletions lib/mumukit/auth/permissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,36 @@ def initialize(scopes={})
@scopes = scopes.with_indifferent_access
end

def has_permission?(role, resource_slug)
Mumukit::Auth::Role.parse(role).allows?(resource_slug, self)
# Deprecated: use `allows` or `authorizes?` instead
def has_permission?(role, thing)
warn "Don't use has_permission?\n" +
"Use allows? if want to validate a slug-like object\n" +
"Use authorizes? if you want to validate an authorizable - grant-like or slug-like - object"
if thing.is_a?(Mumukit::Auth::Grant::Base)
warn "Using authorizes?"
authorizes?(role, thing)
else
warn "Using allows?"
allows?(role, thing)
end
end

# tells wether this permissions
# authorize the given authorizable object for the given role,
# or any of its parent roles.
def authorizes?(role, authorizable)
Mumukit::Auth::Role.parse(role).authorizes?(authorizable, self)
end

# Similar to `authorizes?`, but specialized for slug-like objects
def allows?(role, slug_like)
authorizes? role, slug_like.to_mumukit_slug
end

def role_allows?(role, resource_slug)
scope_for(role).allows?(resource_slug)
# tells wether this permissions
# authorize the given authorizable object for the specific given role
def role_authorizes?(role, authorizable)
scope_for(role).authorizes?(authorizable)
end

def has_role?(role)
Expand All @@ -42,7 +66,7 @@ def student_granted_organizations
end

def granted_organizations_for(role)
scope_for(role)&.grants&.map { |grant| grant.to_mumukit_slug.organization }.to_set
scope_for(role)&.grants&.flat_map { |grant| grant.granted_organizations }.to_set
end

def add_permission!(role, *grants)
Expand All @@ -63,7 +87,7 @@ def update_permission!(role, old_grant, new_grant)
end

def delegate_to?(other)
other.scopes.all? { |role, scope| has_all_permissions?(role, scope) }
other.scopes.all? { |role, scope| authorizes_all?(role, scope) }
end

def grant_strings_for(role)
Expand Down Expand Up @@ -99,7 +123,7 @@ def self.dump(permission)

def assign_to?(other, previous)
diff = previous.as_set ^ other.as_set
diff.all? { |role, grant| has_permission?(role, grant) }
diff.all? { |role, grant| authorizes?(role, grant) }
end

def protect_permissions_assignment!(other, previous)
Expand Down Expand Up @@ -134,8 +158,8 @@ def to_h

private

def has_all_permissions?(role, scope)
scope.grants.all? { |grant| has_permission? role, grant }
def authorizes_all?(role, scope)
scope.grants.all? { |grant| authorizes? role, grant }
end

end
6 changes: 3 additions & 3 deletions lib/mumukit/auth/protection.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
module Mumukit::Auth::Protection
def protect!(role, slug)
def protect!(role, slug_like)
raise Mumukit::Auth::UnauthorizedAccessError,
"Unauthorized access to #{slug} as #{role}. Scope is `#{scope_for role}`" unless has_permission?(role, slug)
"Unauthorized access to #{slug_like} as #{role}. Scope is `#{scope_for role}`" unless allows?(role, slug_like)
end

def protect_delegation!(other)
other ||= {}
raise Mumukit::Auth::UnauthorizedAccessError,
"Unauthorized delegation to #{other.to_h}" unless delegate_to?(Mumukit::Auth::Permissions.parse(other.to_h))
end
end
end
19 changes: 12 additions & 7 deletions lib/mumukit/auth/role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ def initialize(symbol)
@symbol=symbol
end

def allows?(resource_slug, permissions)
permissions.role_allows?(to_sym, resource_slug) ||
parent_allows?(resource_slug, permissions)
# Tells wether the given authorizable object
# can be authorized using the given permissions
# by this role or its parent role
#
# This definition is recursive, thus traversing the whole ancenstry chain
def authorizes?(authorizable, permissions)
permissions.role_authorizes?(to_sym, authorizable) ||
parent_authorizes?(authorizable, permissions)
end

def parent_allows?(resource_slug, permissions)
parent.allows?(resource_slug, permissions)
def parent_authorizes?(authorizable, permissions)
parent.authorizes?(authorizable, permissions)
end

def to_sym
Expand Down Expand Up @@ -52,9 +57,9 @@ class Moderator < Role
class Owner < Role
parent nil

def parent_allows?(*)
def parent_authorizes?(*)
false
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/mumukit/auth/roles.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Roles

ROLES.each do |role|
define_method "#{role}?" do |scope = Mumukit::Auth::Slug.any|
has_permission? role.to_sym, scope
allows? role.to_sym, scope
end
end
end
Expand Down
Loading