mirror of
https://github.com/discourse/discourse.git
synced 2024-11-24 07:34:18 +08:00
bfc3132bb2
What is the problem here? In multiple controllers, we are accepting a `limit` params but do not impose any upper bound on the values being accepted. Without an upper bound, we may be allowing arbituary users from generating DB queries which may end up exhausing the resources on the server. What is the fix here? A new `fetch_limit_from_params` helper method is introduced in `ApplicationController` that can be used by controller actions to safely get the limit from the params as a default limit and maximum limit has to be set. When an invalid limit params is encountered, the server will respond with the 400 response code.
492 lines
15 KiB
Ruby
492 lines
15 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe Admin::ApiController do
|
|
fab!(:admin) { Fabricate(:admin) }
|
|
fab!(:moderator) { Fabricate(:moderator) }
|
|
fab!(:user) { Fabricate(:user) }
|
|
|
|
fab!(:key1, refind: false) { Fabricate(:api_key, description: "my key") }
|
|
fab!(:key2, refind: false) { Fabricate(:api_key, user: admin) }
|
|
fab!(:key3, refind: false) { Fabricate(:api_key, user: admin) }
|
|
|
|
describe "#index" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "returns keys successfully" do
|
|
get "/admin/api/keys.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["keys"].length).to eq(3)
|
|
end
|
|
|
|
describe "when limit params is invalid" do
|
|
include_examples "invalid limit params",
|
|
"/admin/api/keys.json",
|
|
described_class::INDEX_LIMIT
|
|
end
|
|
|
|
it "can paginate results" do
|
|
get "/admin/api/keys.json?offset=0&limit=2"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["keys"].map { |x| x["id"] }).to contain_exactly(
|
|
key3.id,
|
|
key2.id,
|
|
)
|
|
|
|
get "/admin/api/keys.json?offset=1&limit=2"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["keys"].map { |x| x["id"] }).to contain_exactly(
|
|
key2.id,
|
|
key1.id,
|
|
)
|
|
|
|
get "/admin/api/keys.json?offset=2&limit=2"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["keys"].map { |x| x["id"] }).to contain_exactly(key1.id)
|
|
end
|
|
end
|
|
|
|
shared_examples "keys inaccessible" do
|
|
it "denies keys access with a 404 response" do
|
|
get "/admin/api/keys.json"
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(response.parsed_body["keys"]).to be_nil
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "keys inaccessible"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "keys inaccessible"
|
|
end
|
|
end
|
|
|
|
describe "#show" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "returns key successfully" do
|
|
get "/admin/api/keys/#{key1.id}.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
data = response.parsed_body["key"]
|
|
expect(data["id"]).to eq(key1.id)
|
|
expect(data["key"]).to eq(nil)
|
|
expect(data["truncated_key"]).to eq(key1.key[0..3])
|
|
expect(data["description"]).to eq("my key")
|
|
end
|
|
end
|
|
|
|
shared_examples "key inaccessible" do
|
|
it "denies key access with a 404 response" do
|
|
get "/admin/api/keys/#{key1.id}.json"
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(response.parsed_body["key"]).to be_nil
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "key inaccessible"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "key inaccessible"
|
|
end
|
|
end
|
|
|
|
describe "#update" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "allows updating the description" do
|
|
original_key = key1.key
|
|
|
|
put "/admin/api/keys/#{key1.id}.json",
|
|
params: {
|
|
key: {
|
|
description: "my new description",
|
|
key: "overridekey",
|
|
},
|
|
}
|
|
expect(response.status).to eq(200)
|
|
|
|
key1.reload
|
|
expect(key1.description).to eq("my new description")
|
|
expect(key1.key).to eq(original_key)
|
|
|
|
expect(UserHistory.last.action).to eq(UserHistory.actions[:api_key_update])
|
|
expect(UserHistory.last.subject).to eq(key1.truncated_key)
|
|
end
|
|
|
|
it "returns 400 for invalid payloads" do
|
|
put "/admin/api/keys/#{key1.id}.json", params: { key: "string not a hash" }
|
|
expect(response.status).to eq(400)
|
|
|
|
put "/admin/api/keys/#{key1.id}.json", params: {}
|
|
expect(response.status).to eq(400)
|
|
end
|
|
end
|
|
|
|
shared_examples "key update not allowed" do
|
|
it "prevents key updates with a 404 response" do
|
|
key1.reload
|
|
original_key = key1.key
|
|
original_description = key1.description
|
|
|
|
put "/admin/api/keys/#{key1.id}.json",
|
|
params: {
|
|
key: {
|
|
description: "my new description",
|
|
key: "overridekey",
|
|
},
|
|
}
|
|
|
|
key1.reload
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(key1.description).to eq(original_description)
|
|
expect(key1.key).to eq(original_key)
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "key update not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "key update not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#destroy" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "works" do
|
|
expect(ApiKey.exists?(key1.id)).to eq(true)
|
|
|
|
delete "/admin/api/keys/#{key1.id}.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(ApiKey.exists?(key1.id)).to eq(false)
|
|
|
|
expect(UserHistory.last.action).to eq(UserHistory.actions[:api_key_destroy])
|
|
expect(UserHistory.last.subject).to eq(key1.truncated_key)
|
|
end
|
|
end
|
|
|
|
shared_examples "key deletion not allowed" do
|
|
it "prevents key deletion with a 404 response" do
|
|
expect(ApiKey.exists?(key1.id)).to eq(true)
|
|
|
|
delete "/admin/api/keys/#{key1.id}.json"
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(ApiKey.exists?(key1.id)).to eq(true)
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "key deletion not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "key deletion not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#create" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "can create a master key" do
|
|
post "/admin/api/keys.json", params: { key: { description: "master key description" } }
|
|
expect(response.status).to eq(200)
|
|
|
|
data = response.parsed_body
|
|
|
|
expect(data["key"]["description"]).to eq("master key description")
|
|
expect(data["key"]["user"]).to eq(nil)
|
|
expect(data["key"]["key"]).to_not eq(nil)
|
|
expect(data["key"]["last_used_at"]).to eq(nil)
|
|
|
|
key = ApiKey.find(data["key"]["id"])
|
|
expect(key.description).to eq("master key description")
|
|
expect(key.user).to eq(nil)
|
|
|
|
expect(UserHistory.last.action).to eq(UserHistory.actions[:api_key_create])
|
|
expect(UserHistory.last.subject).to eq(key.truncated_key)
|
|
end
|
|
|
|
it "can create a user-specific key" do
|
|
user = Fabricate(:user)
|
|
post "/admin/api/keys.json",
|
|
params: {
|
|
key: {
|
|
description: "restricted key description",
|
|
username: user.username,
|
|
},
|
|
}
|
|
expect(response.status).to eq(200)
|
|
|
|
data = response.parsed_body
|
|
|
|
expect(data["key"]["description"]).to eq("restricted key description")
|
|
expect(data["key"]["user"]["username"]).to eq(user.username)
|
|
expect(data["key"]["key"]).to_not eq(nil)
|
|
expect(data["key"]["last_used_at"]).to eq(nil)
|
|
|
|
key = ApiKey.find(data["key"]["id"])
|
|
expect(key.description).to eq("restricted key description")
|
|
expect(key.user.id).to eq(user.id)
|
|
|
|
expect(UserHistory.last.action).to eq(UserHistory.actions[:api_key_create])
|
|
expect(UserHistory.last.subject).to eq(key.truncated_key)
|
|
end
|
|
|
|
describe "Scopes" do
|
|
it "creates an scope with allowed parameters" do
|
|
post "/admin/api/keys.json",
|
|
params: {
|
|
key: {
|
|
description: "master key description",
|
|
scopes: [{ scope_id: "topics:write", topic_id: "55" }],
|
|
},
|
|
}
|
|
expect(response.status).to eq(200)
|
|
|
|
data = response.parsed_body
|
|
scope = ApiKeyScope.find_by(api_key_id: data.dig("key", "id"))
|
|
|
|
expect(scope.resource).to eq("topics")
|
|
expect(scope.action).to eq("write")
|
|
expect(scope.allowed_parameters["topic_id"]).to contain_exactly("55")
|
|
end
|
|
|
|
it "allows multiple parameters separated by a comma" do
|
|
post "/admin/api/keys.json",
|
|
params: {
|
|
key: {
|
|
description: "master key description",
|
|
scopes: [{ scope_id: "topics:write", topic_id: "55,33" }],
|
|
},
|
|
}
|
|
expect(response.status).to eq(200)
|
|
|
|
data = response.parsed_body
|
|
scope = ApiKeyScope.find_by(api_key_id: data.dig("key", "id"))
|
|
|
|
expect(scope.allowed_parameters["topic_id"]).to contain_exactly("55", "33")
|
|
end
|
|
end
|
|
|
|
it "ignores invalid parameters" do
|
|
post "/admin/api/keys.json",
|
|
params: {
|
|
key: {
|
|
description: "master key description",
|
|
scopes: [{ scope_id: "topics:write", fake_id: "55" }],
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
data = response.parsed_body
|
|
scope = ApiKeyScope.find_by(api_key_id: data.dig("key", "id"))
|
|
|
|
expect(scope.allowed_parameters["fake_id"]).to be_nil
|
|
end
|
|
|
|
it "fails when the scope is invalid" do
|
|
post "/admin/api/keys.json",
|
|
params: {
|
|
key: {
|
|
description: "master key description",
|
|
scopes: [{ scope_id: "something:else" }],
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(400)
|
|
end
|
|
end
|
|
|
|
shared_examples "key creation not allowed" do
|
|
it "prevents key creation with a 404 response" do
|
|
post "/admin/api/keys.json", params: { key: { description: "master key description" } }
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(response.parsed_body["key"]).to be_nil
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "key creation not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "key creation not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#revoke and #undo_revoke" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "works correctly" do
|
|
post "/admin/api/keys/#{key1.id}/revoke.json"
|
|
expect(response.status).to eq 200
|
|
|
|
key1.reload
|
|
expect(key1.revoked_at).to_not eq(nil)
|
|
expect(UserHistory.last.action).to eq(UserHistory.actions[:api_key_update])
|
|
expect(UserHistory.last.subject).to eq(key1.truncated_key)
|
|
expect(UserHistory.last.details).to eq(I18n.t("staff_action_logs.api_key.revoked"))
|
|
|
|
post "/admin/api/keys/#{key1.id}/undo-revoke.json"
|
|
expect(response.status).to eq 200
|
|
|
|
key1.reload
|
|
expect(key1.revoked_at).to eq(nil)
|
|
expect(UserHistory.last.action).to eq(UserHistory.actions[:api_key_update])
|
|
expect(UserHistory.last.subject).to eq(key1.truncated_key)
|
|
expect(UserHistory.last.details).to eq(I18n.t("staff_action_logs.api_key.restored"))
|
|
end
|
|
end
|
|
|
|
shared_examples "key revocation/revocation undoing not allowed" do
|
|
it "prevents revoking/un-revoking key with a 404 response" do
|
|
key1.reload
|
|
post "/admin/api/keys/#{key1.id}/revoke.json"
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(key1.revoked_at).to eq(nil)
|
|
|
|
post "/admin/api/keys/#{key1.id}/undo-revoke.json"
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(key1.revoked_at).to eq(nil)
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "key revocation/revocation undoing not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "key revocation/revocation undoing not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#scopes" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "includes scopes" do
|
|
get "/admin/api/keys/scopes.json"
|
|
|
|
scopes = response.parsed_body["scopes"]
|
|
|
|
expect(scopes.keys).to contain_exactly(
|
|
"topics",
|
|
"users",
|
|
"email",
|
|
"posts",
|
|
"tags",
|
|
"uploads",
|
|
"user_status",
|
|
"global",
|
|
"badges",
|
|
"groups",
|
|
"categories",
|
|
"search",
|
|
"invites",
|
|
"wordpress",
|
|
)
|
|
|
|
topic_routes = [
|
|
"/t/:id (GET)",
|
|
"/t/external_id/:external_id (GET)",
|
|
"/t/:slug/:topic_id/print (GET)",
|
|
"/t/:slug/:topic_id/summary (GET)",
|
|
"/t/:topic_id/summary (GET)",
|
|
"/t/:topic_id/:post_number (GET)",
|
|
"/t/:topic_id/last (GET)",
|
|
"/t/:slug/:topic_id.rss (GET)",
|
|
"/t/:slug/:topic_id (GET)",
|
|
"/t/:slug/:topic_id/:post_number (GET)",
|
|
"/t/:slug/:topic_id/last (GET)",
|
|
"/t/:topic_id/posts (GET)",
|
|
"/latest.rss (GET)",
|
|
]
|
|
|
|
topic_routes.each do |route|
|
|
expect(scopes["topics"].any? { |h| h["urls"].include?(route) }).to be_truthy
|
|
end
|
|
|
|
expect(scopes["posts"].any? { |h| h["urls"].include?("/posts (GET)") }).to be_truthy
|
|
expect(scopes["posts"].any? { |h| h["urls"].include?("/private-posts (GET)") }).to be_truthy
|
|
end
|
|
end
|
|
|
|
shared_examples "key scopes inaccessible" do
|
|
it "denies key scopes access with a 404 response" do
|
|
get "/admin/api/keys/scopes.json"
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(response.parsed_body["scopes"]).to be_nil
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "key scopes inaccessible"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "key scopes inaccessible"
|
|
end
|
|
end
|
|
end
|