grim/prosody_mod_auth_jetbrains_hub

58723e5d2735
Update my copyright and the readme for hub_groups being required now
-- Prosody IM
-- Copyright (C) 2008-2013 Matthew Wild
-- Copyright (C) 2008-2013 Waqas Hussain
-- Copyright (C) 2014 Kim Alvefur
-- Copyright (C) 2019-2021 Gary Kramlich
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local new_sasl = require "util.sasl".new;
local base64 = require "util.encodings".base64.encode;
local json_decode = require "util.json".decode
local formencode = require "util.http".formencode
local have_async, async = pcall(require, "util.async");
local collect = require "util.array".collect
local log = module._log;
local host = module.host;
-- Options
local hub_url = module:get_option_string("jetbrains_hub_url"):gsub("$host", host);
if not hub_url then error("jetbrains_hub_url required") end
local hub_scopes = module:get_option_string("jetbrains_hub_scopes");
if not hub_scopes then error("jetbrains_hub_scopes required") end
local hub_client_id = module:get_option_string("jetbrains_hub_client_id");
if not hub_client_id then error("jetbrains_hub_client_id required") end
local hub_client_secret = module:get_option_string("jetbrains_hub_client_secret");
if not hub_client_secret then error("jetbrains_hub_client_secret required") end
local hub_groups = module:get_option_set("jetbrains_hub_groups", {});
if hub_groups:empty() then error("jetbrains_hub_groups must specify at least one group") end
-- we only need to update the group part of the query once on load so do it
-- right away
local group_query = collect(hub_groups):map(
function(item)
return string.format("in:%q", item)
end
):concat(" or ")
local provider = {};
-- globals required by socket.http
if rawget(_G, "PROXY") == nil then
rawset(_G, "PROXY", false)
end
if rawget(_G, "base_parsed") == nil then
rawset(_G, "base_parsed", false)
end
local http_request = nil
if have_async then
http_request = function(url, headers, body)
local http = require "net.http";
local wait, done = async.waiter();
local code, response;
local ex = {
headers = headers;
}
if body and #body > 0 then
ex["body"] = formencode(body);
end
local function cb(content_, code_, response_, request_)
code, response = code_, response_.body;
done();
end
http.request(url, ex, cb);
wait();
return {
code = code;
content = response;
}
end
else
prosody.unlock_globals()
require "ltn12"
require "socket"
require "socket.http"
require "ssl.https"
prosody.lock_globals()
http_request = function(url, headers, body)
local http = require "socket.http";
local https = require "ssl.https";
local request;
if string.sub(url, 1, string.len('https')) == 'https' then
request = https.request;
else
request = http.request;
end
local t = {}
local _, code, _ = request{
url = url,
headers = headers,
sink = ltn12.sink.table(t),
};
if body and #body > 0 then
request['body'] = formencode(body)
end
return {
code = code,
content = table.concat(t),
}
end
end
local function check_group(username, token)
local headers = {
Authorization = "Bearer "..token;
};
local params = {
{ name = "$top", value = "1" };
{ name = "query", value = string.format("login:%s and (%s)", username, group_query )};
{ name = "fields", value = "login" };
}
local url = string.format('%s/api/rest/users?%s', hub_url, formencode(params))
local resp = http_request(url, headers, nil)
if resp.code == 200 then
local json_payload = json_decode(resp.content);
if json_payload.total ~= 1 then
module:log("warn", "user %q authenticated but is not in any approved groups", username)
return nil, "Permission denied"
end
if json_payload.users[1]['login'] ~= username then
module:log("warn", "user %q authenticated is in an approved group but the returned username was different", username)
return nil, "Permission denied"
end
return true;
end
return nil, "Failed to check access permissions"
end
function provider.test_password(username, password)
module:log("debug", "testing password for jbhub auth")
local headers = {
Authorization = "Basic "..base64(hub_client_id..":"..hub_client_secret);
};
local body = {
{ name = "grant_type", value = "password" };
{ name = "username", value = username };
{ name = "password", value = password };
{ name = "scope", value = hub_scopes };
}
local resp = http_request(hub_url.."/api/rest/oauth2/token", headers, body);
if resp.code >= 200 and resp.code <= 299 then
module:log("debug", "HTTP auth provider confirmed valid password");
-- a group is required to we need to do that check now.
local json_payload = json_decode(resp.content);
return check_group(username, json_payload.access_token)
else
module:log("debug", "HTTP auth provider returned status code %d", resp.code);
end
return nil, "Auth failed. Invalid username or password.";
end
function provider.users()
return function()
return nil;
end
end
function provider.set_password(username, password)
return nil, "Changing passwords not supported";
end
function provider.user_exists(username)
return true;
end
function provider.create_user(username, password)
return nil, "User creation not supported";
end
function provider.delete_user(username)
return nil , "User deletion not supported";
end
function provider.get_sasl_handler()
return new_sasl(host, {
plain_test = function(sasl, username, password, realm)
return provider.test_password(username, password), true;
end
});
end
module:provides("auth", provider);