mirror of
https://github.com/discourse/discourse.git
synced 2025-06-06 03:06:53 +08:00

Currently, after creating an API key, there is no way in the UI to see what scope the key has. To do this we need to first store the selected scope mode when creating a new key. In this PR we: - Convert scope_mode from a transient attribute to a database backed enum. - Ship the possible values through the javascript:update_constants rake task instead of hard coding in front-end. In follow-up PRs we will: - Backfill existing API keys based on their associated api_key_scopes records. - Start showing the scope mode in the UI.
193 lines
6.3 KiB
Ruby
193 lines
6.3 KiB
Ruby
# encoding: utf-8
|
|
# frozen_string_literal: true
|
|
|
|
RSpec.describe ApiKey do
|
|
fab!(:user)
|
|
|
|
it { is_expected.to belong_to :user }
|
|
it { is_expected.to belong_to :created_by }
|
|
it { is_expected.to validate_length_of(:description).is_at_most(255) }
|
|
it { is_expected.to define_enum_for(:scope_mode).with_values(%w[global read_only granular]) }
|
|
|
|
it "validates at least one scope for granular mode" do
|
|
api_key = ApiKey.new
|
|
api_key.scope_mode = "granular"
|
|
|
|
api_key.validate
|
|
|
|
expect(api_key.errors).to contain_exactly("Api key scopes at least one must be selected")
|
|
end
|
|
|
|
it "generates a key when saving" do
|
|
api_key = ApiKey.new
|
|
api_key.save!
|
|
initial_key = api_key.key
|
|
expect(initial_key.length).to eq(64)
|
|
|
|
# Does not overwrite key when saving again
|
|
api_key.description = "My description here"
|
|
api_key.save!
|
|
expect(api_key.reload.key).to eq(initial_key)
|
|
end
|
|
|
|
it "does not have the key when loading later from the database" do
|
|
api_key = ApiKey.create!
|
|
expect(api_key.key_available?).to eq(true)
|
|
expect(api_key.key.length).to eq(64)
|
|
|
|
api_key = ApiKey.find(api_key.id)
|
|
expect(api_key.key_available?).to eq(false)
|
|
expect { api_key.key }.to raise_error(ApiKey::KeyAccessError)
|
|
end
|
|
|
|
it "can lookup keys based on their hash" do
|
|
key = ApiKey.create!.key
|
|
expect(ApiKey.with_key(key).length).to eq(1)
|
|
end
|
|
|
|
it "can calculate the epoch correctly" do
|
|
expect(ApiKey.last_used_epoch.to_datetime).to be_a(DateTime)
|
|
|
|
SiteSetting.api_key_last_used_epoch = ""
|
|
expect(ApiKey.last_used_epoch).to eq(nil)
|
|
end
|
|
|
|
it "can automatically revoke unused keys" do
|
|
now = Time.now
|
|
|
|
SiteSetting.api_key_last_used_epoch = now - 2.years
|
|
SiteSetting.revoke_api_keys_unused_days = 180 # 6 months
|
|
|
|
freeze_time now - 1.year
|
|
never_used = Fabricate(:api_key)
|
|
used_previously = Fabricate(:api_key)
|
|
used_previously.update(last_used_at: Time.zone.now)
|
|
used_recently = Fabricate(:api_key)
|
|
|
|
freeze_time now - 3.months
|
|
used_recently.update(last_used_at: Time.zone.now)
|
|
|
|
freeze_time now
|
|
ApiKey.revoke_unused_keys!
|
|
|
|
[never_used, used_previously, used_recently].each(&:reload)
|
|
expect(never_used.revoked_at).to_not eq(nil)
|
|
expect(used_previously.revoked_at).to_not eq(nil)
|
|
expect(used_recently.revoked_at).to eq(nil)
|
|
|
|
# Restore them
|
|
[never_used, used_previously, used_recently].each { |a| a.update(revoked_at: nil) }
|
|
|
|
# Move the epoch to 1 month ago
|
|
SiteSetting.api_key_last_used_epoch = now - 1.month
|
|
ApiKey.revoke_unused_keys!
|
|
|
|
[never_used, used_previously, used_recently].each(&:reload)
|
|
expect(never_used.revoked_at).to eq(nil)
|
|
expect(used_previously.revoked_at).to eq(nil)
|
|
expect(used_recently.revoked_at).to eq(nil)
|
|
end
|
|
|
|
it "can automatically revoke keys by max life" do
|
|
freeze_time
|
|
|
|
SiteSetting.revoke_api_keys_maxlife_days = 2
|
|
|
|
older_key = Fabricate(:api_key, created_at: 3.days.ago)
|
|
newer_key = Fabricate(:api_key, created_at: 1.days.ago)
|
|
revoked_key = Fabricate(:api_key, created_at: 3.days.ago, revoked_at: 1.day.ago)
|
|
|
|
expect { ApiKey.revoke_max_life_keys! }.to change { older_key.reload.revoked_at }.from(nil).to(
|
|
be_within_one_second_of Time.current
|
|
).and not_change { newer_key.reload.revoked_at }.and not_change {
|
|
revoked_key.reload.revoked_at
|
|
}
|
|
end
|
|
|
|
describe "API Key scope mappings" do
|
|
it "maps api_key permissions" do
|
|
api_key_mappings = ApiKeyScope.scope_mappings[:topics]
|
|
|
|
assert_responds_to(api_key_mappings.dig(:write, :actions))
|
|
assert_responds_to(api_key_mappings.dig(:read, :actions))
|
|
assert_responds_to(api_key_mappings.dig(:read_lists, :actions))
|
|
end
|
|
|
|
def assert_responds_to(mappings)
|
|
mappings.each do |m|
|
|
controller, method = m.split("#")
|
|
controller_name = "#{controller.capitalize}Controller"
|
|
expect(controller_name.constantize.method_defined?(method)).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#request_allowed?" do
|
|
let(:request) do
|
|
ActionDispatch::TestRequest.create.tap do |request|
|
|
request.path_parameters = { controller: "topics", action: "show", topic_id: "3" }
|
|
request.remote_addr = "133.45.67.99"
|
|
end
|
|
end
|
|
|
|
let(:env) { request.env }
|
|
|
|
let(:key) { ApiKey.new(api_key_scopes: [scope]) }
|
|
|
|
context "with regular scopes" do
|
|
let(:scope) do
|
|
ApiKeyScope.new(resource: "topics", action: "read", allowed_parameters: { topic_id: "3" })
|
|
end
|
|
|
|
it "allows the request if there are no allowed IPs" do
|
|
key.allowed_ips = nil
|
|
key.api_key_scopes = []
|
|
expect(key.request_allowed?(env)).to eq(true)
|
|
end
|
|
|
|
it "rejects the request if the IP is not allowed" do
|
|
key.allowed_ips = %w[115.65.76.87]
|
|
expect(key.request_allowed?(env)).to eq(false)
|
|
end
|
|
|
|
it "allow the request if there are not allowed params" do
|
|
scope.allowed_parameters = nil
|
|
expect(key.request_allowed?(env)).to eq(true)
|
|
end
|
|
|
|
it "rejects the request when params are different" do
|
|
request.path_parameters = { controller: "topics", action: "show", topic_id: "4" }
|
|
expect(key.request_allowed?(env)).to eq(false)
|
|
end
|
|
|
|
it "accepts the request if one of the parameters match" do
|
|
request.path_parameters = { controller: "topics", action: "show", topic_id: "4" }
|
|
scope.allowed_parameters = { topic_id: %w[3 4] }
|
|
expect(key.request_allowed?(env)).to eq(true)
|
|
end
|
|
|
|
it "allow the request when the scope has an alias" do
|
|
request.path_parameters = { controller: "topics", action: "show", id: "3" }
|
|
expect(key.request_allowed?(env)).to eq(true)
|
|
end
|
|
|
|
it "rejects the request when the main parameter and the alias are both used" do
|
|
request.path_parameters = { controller: "topics", action: "show", topic_id: "3", id: "3" }
|
|
expect(key.request_allowed?(env)).to eq(false)
|
|
end
|
|
end
|
|
|
|
context "with global:read scope" do
|
|
let(:scope) { ApiKeyScope.new(resource: "global", action: "read") }
|
|
|
|
it "allows only GET requests for global:read" do
|
|
request.request_method = "GET"
|
|
expect(key.request_allowed?(env)).to eq(true)
|
|
|
|
request.request_method = "POST"
|
|
expect(key.request_allowed?(env)).to eq(false)
|
|
end
|
|
end
|
|
end
|
|
end
|