FEATURE: Overhaul of admin API key system (#8284)

- Allow revoking keys without deleting them
- Auto-revoke keys after a period of no use (default 6 months)
- Allow multiple keys per user
- Allow attaching a description to each key, for easier auditing
- Log changes to keys in the staff action log
- Move all key management to one place, and improve the UI
This commit is contained in:
David Taylor
2019-11-05 14:10:23 +00:00
committed by GitHub
parent fa2c06da93
commit 52c5cf33f8
46 changed files with 863 additions and 395 deletions

View File

@ -10,6 +10,9 @@ describe Admin::ApiController do
fab!(:admin) { Fabricate(:admin) }
fab!(:key1) { Fabricate(:api_key, description: "my key") }
fab!(:key2) { Fabricate(:api_key, user: admin) }
context "as an admin" do
before do
sign_in(admin)
@ -19,60 +22,159 @@ describe Admin::ApiController do
it "succeeds" do
get "/admin/api/keys.json"
expect(response.status).to eq(200)
expect(JSON.parse(response.body)["keys"].length).to eq(2)
end
end
describe '#regenerate_key' do
fab!(:api_key) { Fabricate(:api_key) }
it "returns 404 when there is no key" do
put "/admin/api/key.json", params: { id: 1234 }
expect(response.status).to eq(404)
end
it "delegates to the api key's `regenerate!` method" do
prev_value = api_key.key
put "/admin/api/key.json", params: { id: api_key.id }
describe '#show' do
it "succeeds" do
get "/admin/api/keys/#{key1.id}.json"
expect(response.status).to eq(200)
api_key.reload
expect(api_key.key).not_to eq(prev_value)
expect(api_key.created_by.id).to eq(admin.id)
data = JSON.parse(response.body)["key"]
expect(data["id"]).to eq(key1.id)
expect(data["key"]).to eq(key1.key)
expect(data["description"]).to eq("my key")
end
end
describe '#revoke_key' do
fab!(:api_key) { Fabricate(:api_key) }
describe '#update' do
it "allows updating the description" do
original_key = key1.key
it "returns 404 when there is no key" do
delete "/admin/api/key.json", params: { id: 1234 }
expect(response.status).to eq(404)
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 "delegates to the api key's `regenerate!` method" do
delete "/admin/api/key.json", params: { id: api_key.id }
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
describe "#destroy" do
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.where(key: api_key.key).count).to eq(0)
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
describe "#create" do
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 = JSON.parse(response.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 = JSON.parse(response.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
end
describe "#revoke and #undo_revoke" do
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
end
describe '#create_master_key' do
it "creates a record" do
sign_in(admin)
expect do
post "/admin/api/key.json"
end.to change(ApiKey, :count).by(1)
expect(response.status).to eq(200)
end
it "doesn't allow moderators to create master keys" do
context "as a moderator" do
before do
sign_in(Fabricate(:moderator))
expect do
post "/admin/api/key.json"
end.to change(ApiKey, :count).by(0)
expect(response.status).to eq(404)
end
it "doesn't allow access" do
get "/admin/api/keys.json"
expect(response.status).to eq(404)
get "/admin/api/key/#{key1.id}.json"
expect(response.status).to eq(404)
post "/admin/api/keys.json", params: {
key: {
description: "master key description"
}
}
expect(response.status).to eq(404)
expect(ApiKey.count).to eq(2)
end
end
end