diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 000000000..ae1b9e1ed --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,3 @@ +exclude_patterns: +- "lib/doorkeeper/config.rb" +- "spec/" diff --git a/CHANGELOG.md b/CHANGELOG.md index 982d6d323..5595b7693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/doorkeeper/config.rb b/lib/doorkeeper/config.rb index eaa226c75..c464f1803 100644 --- a/lib/doorkeeper/config.rb +++ b/lib/doorkeeper/config.rb @@ -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) @@ -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 diff --git a/lib/doorkeeper/oauth/scopes.rb b/lib/doorkeeper/oauth/scopes.rb index c5b10cf18..21a6f1a91 100644 --- a/lib/doorkeeper/oauth/scopes.rb +++ b/lib/doorkeeper/oauth/scopes.rb @@ -6,6 +6,8 @@ class Scopes include Enumerable include Comparable + DYNAMIC_SCOPE_WILDCARD = "*" + def self.from_string(string) string ||= "" new.tap do |scope| @@ -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) @@ -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 diff --git a/spec/lib/config_spec.rb b/spec/lib/config_spec.rb index e88216250..83970ec89 100644 --- a/spec/lib/config_spec.rb +++ b/spec/lib/config_spec.rb @@ -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) diff --git a/spec/lib/oauth/scopes_spec.rb b/spec/lib/oauth/scopes_spec.rb index f9042e8c2..6f1fc3aba 100644 --- a/spec/lib/oauth/scopes_spec.rb +++ b/spec/lib/oauth/scopes_spec.rb @@ -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