Commit 80c1ebaa authored by Timothy Andrew's avatar Timothy Andrew

Allow API scope declarations to be applied conditionally.

- Scope declarations of the form:

    allow_access_with_scope :read_user, if: -> (request) { request.get? }

  will only apply for `GET` requests

- Add a negative test to a `POST` endpoint in the `users` API to test this. Also
  test for this case in the `AccessTokenValidationService` unit tests.
parent 6f192250
......@@ -5,10 +5,11 @@ class AccessTokenValidationService
REVOKED = :revoked
INSUFFICIENT_SCOPE = :insufficient_scope
attr_reader :token
attr_reader :token, :request
def initialize(token)
def initialize(token, request)
@token = token
@request = request
end
def validate(scopes: [])
......@@ -31,11 +32,13 @@ class AccessTokenValidationService
if scopes.blank?
true
else
#scopes = scopes.reject { |scope| scope[:if].presence && !scope[:if].call(request) }
# Check whether the token is allowed access to any of the required scopes.
# Remove any scopes whose `if` condition does not return `true`
scopes = scopes.reject { |scope| scope[:if].presence && !scope[:if].call(request) }
scope_names = scopes.map { |scope| scope[:name].to_s }
Set.new(scope_names).intersection(Set.new(token.scopes)).present?
# Check whether the token is allowed access to any of the required scopes.
passed_scope_names = scopes.map { |scope| scope[:name].to_sym }
token_scope_names = token.scopes.map(&:to_sym)
Set.new(passed_scope_names).intersection(Set.new(token_scope_names)).present?
end
end
end
......@@ -68,7 +68,7 @@ module API
access_token = find_access_token
return nil unless access_token
case AccessTokenValidationService.new(access_token).validate(scopes: scopes)
case AccessTokenValidationService.new(access_token, request).validate(scopes: scopes)
when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
......@@ -105,7 +105,7 @@ module API
access_token = PersonalAccessToken.active.find_by_token(token_string)
return unless access_token
if AccessTokenValidationService.new(access_token).include_any_scope?(scopes)
if AccessTokenValidationService.new(access_token, request).include_any_scope?(scopes)
User.find(access_token.user_id)
end
end
......
......@@ -340,7 +340,7 @@ module API
end
def initial_current_user
endpoint_class = options[:for]
endpoint_class = options[:for].presence || ::API::API
return @initial_current_user if defined?(@initial_current_user)
Gitlab::Auth::UniqueIpsLimiter.limit_user! do
......
......@@ -14,6 +14,8 @@ describe API::Helpers do
let(:request) { Rack::Request.new(env) }
let(:header) { }
before { allow_any_instance_of(self.class).to receive(:options).and_return({}) }
def set_env(user_or_token, identifier)
clear_env
clear_param
......@@ -167,7 +169,6 @@ describe API::Helpers do
it "returns nil for a token without the appropriate scope" do
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_access_with_scope('write_user')
expect(current_user).to be_nil
end
......
......@@ -321,6 +321,16 @@ describe API::Users do
.to eq([Gitlab::PathRegex.namespace_format_message])
end
context 'when the requesting token has the "read_user" scope' do
let(:token) { create(:personal_access_token, scopes: ['read_user'], user: admin) }
it 'returns a "401" response' do
post api("/users", admin, personal_access_token: token), attributes_for(:user, projects_limit: 3)
expect(response).to have_http_status(401)
end
end
it "is not available for non admin users" do
post api("/users", user), attributes_for(:user)
expect(response).to have_http_status(403)
......
......@@ -2,40 +2,64 @@ require 'spec_helper'
describe AccessTokenValidationService, services: true do
describe ".include_any_scope?" do
let(:request) { double("request") }
it "returns true if the required scope is present in the token's scopes" do
token = double("token", scopes: [:api, :read_user])
expect(described_class.new(token).include_any_scope?([:api])).to be(true)
expect(described_class.new(token, request).include_any_scope?([{ name: :api }])).to be(true)
end
it "returns true if more than one of the required scopes is present in the token's scopes" do
token = double("token", scopes: [:api, :read_user, :other_scope])
expect(described_class.new(token).include_any_scope?([:api, :other_scope])).to be(true)
expect(described_class.new(token, request).include_any_scope?([{ name: :api }, { name: :other_scope }])).to be(true)
end
it "returns true if the list of required scopes is an exact match for the token's scopes" do
token = double("token", scopes: [:api, :read_user, :other_scope])
expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true)
expect(described_class.new(token, request).include_any_scope?([{ name: :api }, { name: :read_user }, { name: :other_scope }])).to be(true)
end
it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do
token = double("token", scopes: [:api, :read_user])
expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true)
expect(described_class.new(token, request).include_any_scope?([{ name: :api }, { name: :read_user }, { name: :other_scope }])).to be(true)
end
it 'returns true if the list of required scopes is blank' do
token = double("token", scopes: [])
expect(described_class.new(token).include_any_scope?([])).to be(true)
expect(described_class.new(token, request).include_any_scope?([])).to be(true)
end
it "returns false if there are no scopes in common between the required scopes and the token scopes" do
token = double("token", scopes: [:api, :read_user])
expect(described_class.new(token).include_any_scope?([:other_scope])).to be(false)
expect(described_class.new(token, request).include_any_scope?([{ name: :other_scope }])).to be(false)
end
context "conditions" do
context "if" do
it "ignores any scopes whose `if` condition returns false" do
token = double("token", scopes: [:api, :read_user])
expect(described_class.new(token, request).include_any_scope?([{ name: :api, if: ->(_) { false } }])).to be(false)
end
it "does not ignore scopes whose `if` condition is not set" do
token = double("token", scopes: [:api, :read_user])
expect(described_class.new(token, request).include_any_scope?([{ name: :api, if: ->(_) { false } }, { name: :read_user }])).to be(true)
end
it "does not ignore scopes whose `if` condition returns true" do
token = double("token", scopes: [:api, :read_user])
expect(described_class.new(token, request).include_any_scope?([{ name: :api, if: ->(_) { true } }, { name: :read_user, if: ->(_) { false } }])).to be(true)
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment