Skip to content

Commit

Permalink
Merge pull request #1739 from stanhu/sh-support-dynamic-scopes
Browse files Browse the repository at this point in the history
Add support for dynamic scopes
  • Loading branch information
nbulaj authored Oct 30, 2024
2 parents c113fa3 + 4a0f7ce commit 5057044
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exclude_patterns:
- "lib/doorkeeper/config.rb"
- "spec/"
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ User-visible changes worth mentioning.

Add your entry here.

- [#1739] Add support for dynamic scopes
- [#1715] Fix token introspection invalid request reason
- [#1714] Fix `Doorkeeper::AccessToken.find_or_create_for` with empty scopes which raises NoMethodError
- [#1712] Add `Pragma: no-cache` to token response
Expand Down
18 changes: 18 additions & 0 deletions lib/doorkeeper/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ def confirm_application_owner
@config.instance_variable_set(:@confirm_application_owner, true)
end

# Provide support for dynamic scopes (e.g. user:*) (disabled by default)
# Optional parameter delimiter (default ":") if you want to customize
# the delimiter separating the scope name and matching value.
#
# @param opts [Hash] the options to configure dynamic scopes
def enable_dynamic_scopes(opts = {})
@config.instance_variable_set(:@enable_dynamic_scopes, true)
@config.instance_variable_set(:@dynamic_scopes_delimiter, opts[:delimiter] || ':')
end

# Define default access token scopes for your provider
#
# @param scopes [Array] Default set of access (OAuth::Scopes.new)
Expand Down Expand Up @@ -511,6 +521,14 @@ def enable_application_owner?
option_set? :enable_application_owner
end

def enable_dynamic_scopes?
option_set? :enable_dynamic_scopes
end

def dynamic_scopes_delimiter
@dynamic_scopes_delimiter
end

def polymorphic_resource_owner?
option_set? :polymorphic_resource_owner
end
Expand Down
38 changes: 37 additions & 1 deletion lib/doorkeeper/oauth/scopes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ class Scopes
include Enumerable
include Comparable

DYNAMIC_SCOPE_WILDCARD = "*"

def self.from_string(string)
string ||= ""
new.tap do |scope|
Expand All @@ -26,7 +28,15 @@ def initialize
end

def exists?(scope)
@scopes.include? scope.to_s
scope = scope.to_s

@scopes.any? do |allowed_scope|
if dynamic_scopes_enabled? && dynamic_scopes_present?(allowed_scope, scope)
dynamic_scope_match?(allowed_scope, scope)
else
allowed_scope == scope
end
end
end

def add(*scopes)
Expand Down Expand Up @@ -66,6 +76,32 @@ def &(other)

private

def dynamic_scopes_enabled?
Doorkeeper.config.enable_dynamic_scopes?
end

def dynamic_scope_delimiter
return unless dynamic_scopes_enabled?

@dynamic_scope_delimiter ||= Doorkeeper.config.dynamic_scopes_delimiter
end

def dynamic_scopes_present?(allowed, requested)
allowed.include?(dynamic_scope_delimiter) && requested.include?(dynamic_scope_delimiter)
end

def dynamic_scope_match?(allowed, requested)
allowed_pattern = allowed.split(dynamic_scope_delimiter, 2)
request_pattern = requested.split(dynamic_scope_delimiter, 2)

return false if allowed_pattern[0] != request_pattern[0]
return false if allowed_pattern[1].blank?
return false if request_pattern[1].blank?
return true if allowed_pattern[1] == DYNAMIC_SCOPE_WILDCARD && allowed_pattern[1].present?

allowed_pattern[1] == request_pattern[1]
end

def to_array(other)
case other
when Scopes
Expand Down
32 changes: 32 additions & 0 deletions spec/lib/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,38 @@
end
end

describe "enable_dynamic_scopes" do
it "is disabled by default" do
expect(Doorkeeper.config.enable_dynamic_scopes?).not_to be(true)
end

context "when enabled with default delimiter" do
before do
Doorkeeper.configure do
enable_dynamic_scopes
end
end

it 'returns true' do
expect(Doorkeeper.config.enable_dynamic_scopes?).to be(true)
expect(Doorkeeper.config.dynamic_scopes_delimiter).to eq(":")
end
end

context "when enabled with custom delimiter" do
before do
Doorkeeper.configure do
enable_dynamic_scopes(delimiter: "-")
end
end

it 'returns true' do
expect(Doorkeeper.config.enable_dynamic_scopes?).to be(true)
expect(Doorkeeper.config.dynamic_scopes_delimiter).to eq("-")
end
end
end

describe "enable_application_owner" do
it "is disabled by default" do
expect(Doorkeeper.config.enable_application_owner?).not_to be(true)
Expand Down
90 changes: 90 additions & 0 deletions spec/lib/oauth/scopes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,95 @@
it "is false if no scopes are included even for existing ones" do
expect(scopes).not_to have_scopes(described_class.from_string("public admin notexistent"))
end

context "with dynamic scopes disabled" do
context "with wildcard dynamic scope" do
before do
scopes.add "user:*"
end

it "returns false with specific user" do
expect(scopes).not_to have_scopes(described_class.from_string("public user:1"))
end

it "returns true with wildcard user" do
expect(scopes).to have_scopes(described_class.from_string("public user:*"))
end

it "returns false if requested scope missing parameter" do
expect(scopes).not_to have_scopes(described_class.from_string("public user:"))
end
end
end

context "with dynamic scopes enabled" do
before do
Doorkeeper.configure do
enable_dynamic_scopes
end
end

context "with wildcard dynamic scope" do
before do
scopes.add "user:*"
end

it "returns true with specific user" do
expect(scopes).to have_scopes(described_class.from_string("public user:1"))
end

it "returns true with wildcard user" do
expect(scopes).to have_scopes(described_class.from_string("public user:*"))
end

it "returns false if requested scope missing parameter" do
expect(scopes).not_to have_scopes(described_class.from_string("public user:"))
end

it "returns false if dynamic scope does not match" do
expect(scopes).not_to have_scopes(described_class.from_string("public userA:1"))
end
end

context "with specific dynamic scope" do
before do
scopes.add "user:1"
end

it "returns true with specific user" do
expect(scopes).to have_scopes(described_class.from_string("public user:1"))
end

it "returns false with wildcard user" do
expect(scopes).not_to have_scopes(described_class.from_string("public user:*"))
end

it "returns false for disallowed user" do
expect(scopes).not_to have_scopes(described_class.from_string("public user:2"))
end

context "with custom delimiter" do
before do
Doorkeeper.configure do
enable_dynamic_scopes(delimiter: "-")
end

scopes.add "user-1"
end

it "returns true with specific user" do
expect(scopes).to have_scopes(described_class.from_string("public user-1"))
end

it "returns false with wildcard user" do
expect(scopes).not_to have_scopes(described_class.from_string("public user-*"))
end

it "returns false for disallowed user" do
expect(scopes).not_to have_scopes(described_class.from_string("public user-2"))
end
end
end
end
end
end

0 comments on commit 5057044

Please sign in to comment.