discourse/spec/requests/user_api_keys_controller_spec.rb
Sam Saffron 4ea21fa2d0 DEV: use #frozen_string_literal: true on all spec
This change both speeds up specs (less strings to allocate) and helps catch
cases where methods in Discourse are mutating inputs.

Overall we will be migrating everything to use #frozen_string_literal: true
it will take a while, but this is the first and safest move in this direction
2019-04-30 10:27:42 +10:00

339 lines
11 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
describe UserApiKeysController do
let :public_key do
<<~TXT
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDh7BS7Ey8hfbNhlNAW/47pqT7w
IhBz3UyBYzin8JurEQ2pY9jWWlY8CH147KyIZf1fpcsi7ZNxGHeDhVsbtUKZxnFV
p16Op3CHLJnnJKKBMNdXMy0yDfCAHZtqxeBOTcCo1Vt/bHpIgiK5kmaekyXIaD0n
w0z/BYpOgZ8QwnI5ZwIDAQAB
-----END PUBLIC KEY-----
TXT
end
let :private_key do
<<~TXT
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDh7BS7Ey8hfbNhlNAW/47pqT7wIhBz3UyBYzin8JurEQ2pY9jW
WlY8CH147KyIZf1fpcsi7ZNxGHeDhVsbtUKZxnFVp16Op3CHLJnnJKKBMNdXMy0y
DfCAHZtqxeBOTcCo1Vt/bHpIgiK5kmaekyXIaD0nw0z/BYpOgZ8QwnI5ZwIDAQAB
AoGAeHesbjzCivc+KbBybXEEQbBPsThY0Y+VdgD0ewif2U4UnNhzDYnKJeTZExwQ
vAK2YsRDV3KbhljnkagQduvmgJyCKuV/CxZvbJddwyIs3+U2D4XysQp3e1YZ7ROr
YlOIoekHCx1CNm6A4iImqGxB0aJ7Owdk3+QSIaMtGQWaPTECQQDz2UjJ+bomguNs
zdcv3ZP7W3U5RG+TpInSHiJXpt2JdNGfHItozGJCxfzDhuKHK5Cb23bgldkvB9Xc
p/tngTtNAkEA7S4cqUezA82xS7aYPehpRkKEmqzMwR3e9WeL7nZ2cdjZAHgXe49l
3mBhidEyRmtPqbXo1Xix8LDuqik0IdnlgwJAQeYTnLnHS8cNjQbnw4C/ECu8Nzi+
aokJ0eXg5A0tS4ttZvGA31Z0q5Tz5SdbqqnkT6p0qub0JZiZfCNNdsBe9QJAaGT5
fJDwfGYW+YpfLDCV1bUFhMc2QHITZtSyxL0jmSynJwu02k/duKmXhP+tL02gfMRy
vTMorxZRllgYeCXeXQJAEGRXR8/26jwqPtKKJzC7i9BuOYEagqj0nLG2YYfffCMc
d3JGCf7DMaUlaUE8bJ08PtHRJFSGkNfDJLhLKSjpbw==
-----END RSA PRIVATE KEY-----
TXT
end
let :args do
{
scopes: 'read',
client_id: "x" * 32,
auth_redirect: 'http://over.the/rainbow',
application_name: 'foo',
public_key: public_key,
nonce: SecureRandom.hex
}
end
context 'new' do
it "supports a head request cleanly" do
head "/user-api-key/new"
expect(response.status).to eq(200)
expect(response.headers["Auth-Api-Version"]).to eq("4")
end
end
context 'create' do
it "does not allow anon" do
post "/user-api-key.json", params: args
expect(response.status).to eq(403)
end
it "refuses to redirect to disallowed place" do
sign_in(Fabricate(:user))
post "/user-api-key.json", params: args
expect(response.status).to eq(403)
end
it "will allow tokens for staff without TL" do
SiteSetting.min_trust_level_for_user_api_key = 2
SiteSetting.allowed_user_api_auth_redirects = args[:auth_redirect]
user = Fabricate(:user, trust_level: 1, moderator: true)
sign_in(user)
post "/user-api-key.json", params: args
expect(response.status).to eq(302)
end
it "will not create token unless TL is met" do
SiteSetting.min_trust_level_for_user_api_key = 2
SiteSetting.allowed_user_api_auth_redirects = args[:auth_redirect]
user = Fabricate(:user, trust_level: 1)
sign_in(user)
post "/user-api-key.json", params: args
expect(response.status).to eq(403)
end
it "will deny access if requesting more rights than allowed" do
SiteSetting.min_trust_level_for_user_api_key = 0
SiteSetting.allowed_user_api_auth_redirects = args[:auth_redirect]
SiteSetting.allow_user_api_key_scopes = "write"
user = Fabricate(:user, trust_level: 0)
sign_in(user)
post "/user-api-key.json", params: args
expect(response.status).to eq(403)
end
it "allows for a revoke with no id" do
key = Fabricate(:readonly_user_api_key)
post "/user-api-key/revoke.json", headers: { HTTP_USER_API_KEY: key.key }
expect(response.status).to eq(200)
key.reload
expect(key.revoked_at).not_to eq(nil)
end
it "will not allow readonly api keys to revoke others" do
key1 = Fabricate(:readonly_user_api_key)
key2 = Fabricate(:readonly_user_api_key)
post "/user-api-key/revoke.json",
params: { id: key2.id },
headers: { HTTP_USER_API_KEY: key1.key }
expect(response.status).to eq(403)
end
it "will allow readonly api keys to revoke self" do
key = Fabricate(:readonly_user_api_key)
post "/user-api-key/revoke.json",
params: { id: key.id },
headers: { HTTP_USER_API_KEY: key.key }
expect(response.status).to eq(200)
key.reload
expect(key.revoked_at).not_to eq(nil)
end
it "will not return p access if not yet configured" do
SiteSetting.min_trust_level_for_user_api_key = 0
SiteSetting.allowed_user_api_auth_redirects = args[:auth_redirect]
args[:scopes] = "push,read"
args[:push_url] = "https://push.it/here"
user = Fabricate(:user, trust_level: 0)
sign_in(user)
post "/user-api-key.json", params: args
expect(response.status).to eq(302)
uri = URI.parse(response.redirect_url)
query = uri.query
payload = query.split("payload=")[1]
encrypted = Base64.decode64(CGI.unescape(payload))
key = OpenSSL::PKey::RSA.new(private_key)
parsed = JSON.parse(key.private_decrypt(encrypted))
expect(parsed["nonce"]).to eq(args[:nonce])
expect(parsed["push"]).to eq(false)
expect(parsed["api"]).to eq(4)
key = user.user_api_keys.first
expect(key.scopes).to include("push")
expect(key.push_url).to eq("https://push.it/here")
end
it "will redirect correctly with valid token" do
SiteSetting.min_trust_level_for_user_api_key = 0
SiteSetting.allowed_user_api_auth_redirects = args[:auth_redirect]
SiteSetting.allowed_user_api_push_urls = "https://push.it/here"
args[:scopes] = "push,notifications,message_bus,session_info,one_time_password"
args[:push_url] = "https://push.it/here"
user = Fabricate(:user, trust_level: 0)
sign_in(user)
post "/user-api-key.json", params: args
expect(response.status).to eq(302)
uri = URI.parse(response.redirect_url)
query = uri.query
payload = query.split("payload=")[1]
encrypted = Base64.decode64(CGI.unescape(payload))
key = OpenSSL::PKey::RSA.new(private_key)
parsed = JSON.parse(key.private_decrypt(encrypted))
expect(parsed["nonce"]).to eq(args[:nonce])
expect(parsed["push"]).to eq(true)
api_key = UserApiKey.find_by(key: parsed["key"])
expect(api_key.user_id).to eq(user.id)
expect(api_key.scopes.sort).to eq(["push", "message_bus", "notifications", "session_info", "one_time_password"].sort)
expect(api_key.push_url).to eq("https://push.it/here")
uri.query = ""
expect(uri.to_s).to eq(args[:auth_redirect] + "?")
# should overwrite if needed
args["access"] = "pr"
post "/user-api-key.json", params: args
expect(response.status).to eq(302)
one_time_password = query.split("oneTimePassword=")[1]
encrypted_otp = Base64.decode64(CGI.unescape(one_time_password))
parsed_otp = key.private_decrypt(encrypted_otp)
redis_key = "otp_#{parsed_otp}"
expect($redis.get(redis_key)).to eq(user.username)
end
it "will just show the payload if no redirect" do
user = Fabricate(:user, trust_level: 0)
sign_in(user)
args.delete(:auth_redirect)
SiteSetting.min_trust_level_for_user_api_key = 0
post "/user-api-key", params: args
expect(response.status).not_to eq(302)
payload = Nokogiri::HTML(response.body).at('code').content
encrypted = Base64.decode64(payload)
key = OpenSSL::PKey::RSA.new(private_key)
parsed = JSON.parse(key.private_decrypt(encrypted))
api_key = UserApiKey.find_by(key: parsed["key"])
expect(api_key.user_id).to eq(user.id)
end
it "will just show the JSON payload if no redirect" do
user = Fabricate(:user, trust_level: 0)
sign_in(user)
args.delete(:auth_redirect)
SiteSetting.min_trust_level_for_user_api_key = 0
post "/user-api-key.json", params: args
expect(response.status).not_to eq(302)
payload = JSON.parse(response.body)["payload"]
encrypted = Base64.decode64(payload)
key = OpenSSL::PKey::RSA.new(private_key)
parsed = JSON.parse(key.private_decrypt(encrypted))
api_key = UserApiKey.find_by(key: parsed["key"])
expect(api_key.user_id).to eq(user.id)
end
it "will allow redirect to wildcard urls" do
SiteSetting.allowed_user_api_auth_redirects = args[:auth_redirect] + '/*'
args[:auth_redirect] = args[:auth_redirect] + '/bluebirds/fly'
sign_in(Fabricate(:user))
post "/user-api-key.json", params: args
expect(response.status).to eq(302)
end
end
context '#create-one-time-password' do
let :otp_args do
{
auth_redirect: 'http://somewhere.over.the/rainbow',
application_name: 'foo',
public_key: public_key
}
end
it "does not allow anon" do
post "/user-api-key/otp", params: otp_args
expect(response.status).to eq(403)
end
it "refuses to redirect to disallowed place" do
sign_in(Fabricate(:user))
post "/user-api-key/otp", params: otp_args
expect(response.status).to eq(403)
end
it "will allow one-time-password for staff without TL" do
SiteSetting.min_trust_level_for_user_api_key = 2
SiteSetting.allowed_user_api_auth_redirects = otp_args[:auth_redirect]
user = Fabricate(:user, trust_level: 1, moderator: true)
sign_in(user)
post "/user-api-key/otp", params: otp_args
expect(response.status).to eq(302)
end
it "will not allow one-time-password unless TL is met" do
SiteSetting.min_trust_level_for_user_api_key = 2
SiteSetting.allowed_user_api_auth_redirects = otp_args[:auth_redirect]
user = Fabricate(:user, trust_level: 1)
sign_in(user)
post "/user-api-key/otp", params: otp_args
expect(response.status).to eq(403)
end
it "will not allow one-time-password if one_time_password scope is disallowed" do
SiteSetting.allow_user_api_key_scopes = "read|write"
SiteSetting.allowed_user_api_auth_redirects = otp_args[:auth_redirect]
user = Fabricate(:user)
sign_in(user)
post "/user-api-key/otp", params: otp_args
expect(response.status).to eq(403)
end
it "will return one-time-password when args are valid" do
SiteSetting.allowed_user_api_auth_redirects = otp_args[:auth_redirect]
user = Fabricate(:user)
sign_in(user)
post "/user-api-key/otp", params: otp_args
expect(response.status).to eq(302)
uri = URI.parse(response.redirect_url)
query = uri.query
payload = query.split("oneTimePassword=")[1]
encrypted = Base64.decode64(CGI.unescape(payload))
key = OpenSSL::PKey::RSA.new(private_key)
parsed = key.private_decrypt(encrypted)
expect($redis.get("otp_#{parsed}")).to eq(user.username)
end
end
end