mod_http_admin_api: restructure group-related info in API
- Return the members of the group right in the get_group_by_id
call. This is an O(1) of extra work.
- Remove the groups attribute from get_user_by_name as that is
O(n) of work and rarely immediately needed.
The replacement for the group membership information in the user
is for now to use the group API and iterate; future work may fix
that.
local usermanager = require "core.usermanager";
local json = require "util.json";
module:depends("http");
local invites = module:depends("invites");
local tokens = module:depends("tokenauth");
local mod_pep = module:depends("pep");
local group_store = module:open_store("groups");
local group_memberships = module:open_store("groups", "map");
local json_content_type = "application/json";
local www_authenticate_header = ("Bearer realm=%q"):format(module.host.."/"..module.name);
local function check_credentials(request)
local auth_type, auth_data = string.match(request.headers.authorization or "", "^(%S+)%s(.+)$");
if not (auth_type and auth_data) then
return false;
end
if auth_type == "Bearer" then
local token_info = tokens.get_token_info(auth_data);
if not token_info or not token_info.session then
return false;
end
return token_info.session;
end
return nil;
end
function check_auth(routes)
local function check_request_auth(event)
local session = check_credentials(event.request);
if not session then
event.response.headers.authorization = www_authenticate_header;
return false, 401;
elseif session.auth_scope ~= "prosody:scope:admin" then
return false, 403;
end
event.session = session;
return true;
end
for route, handler in pairs(routes) do
routes[route] = function (event, ...)
local permit, code = check_request_auth(event);
if not permit then
return code;
end
return handler(event, ...);
end;
end
return routes;
end
local function token_info_to_invite_info(token_info)
local additional_data = token_info.additional_data;
local groups = additional_data and additional_data.groups or nil;
local source = additional_data and additional_data.source or nil;
return {
id = token_info.token;
type = token_info.type;
reusable = not not token_info.reusable;
inviter = token_info.inviter;
jid = token_info.jid;
uri = token_info.uri;
landing_page = token_info.landing_page;
created_at = token_info.created_at;
expires = token_info.expires;
groups = groups;
source = source;
};
end
function list_invites(event)
local invites_list = {};
for token, invite in invites.pending_account_invites() do --luacheck: ignore 213/token
table.insert(invites_list, token_info_to_invite_info(invite));
end
table.sort(invites_list, function (a, b)
return a.created_at < b.created_at;
end);
event.response.headers["Content-Type"] = json_content_type;
return json.encode_array(invites_list);
end
function get_invite_by_id(event, invite_id)
local invite = invites.get_account_invite_info(invite_id);
if not invite then
return 404;
end
event.response.headers["Content-Type"] = json_content_type;
return json.encode(token_info_to_invite_info(invite));
end
function create_invite(event)
local invite_options;
local request = event.request;
if request.body and #request.body > 0 then
if request.headers.content_type ~= json_content_type then
module:log("warn", "Invalid content type");
return 400;
end
invite_options = json.decode(event.request.body);
if not invite_options then
module:log("warn", "Invalid JSON");
return 400;
end
else
invite_options = {};
end
local invite;
if invite_options.reusable then
invite = invites.create_group(invite_options.groups, invite_options.ttl, {
source = "admin_api/"..event.session.username;
});
else
invite = invites.create_account(nil, {
source = "admin_api/"..event.session.username;
groups = invite_options.groups;
});
end
if not invite then
return 500;
end
event.response.headers["Content-Type"] = json_content_type;
return json.encode(token_info_to_invite_info(invite));
end
function delete_invite(event, invite_id) --luacheck: ignore 212/event
if not invites.delete_account_invite(invite_id) then
return 404;
end
return 200;
end
local function get_user_info(username)
if not usermanager.user_exists(username, module.host) then
return nil;
end
local display_name;
do
local pep_service = mod_pep.get_pep_service(username);
local ok, _, nick_item = pep_service:get_last_item("http://jabber.org/protocol/nick", true);
if ok and nick_item then
display_name = nick_item:get_child_text("nick", "http://jabber.org/protocol/nick");
end
end
return {
username = username;
display_name = display_name;
};
end
local function get_user_groups(username)
local groups;
do
local group_set = group_memberships:get_all(username);
if group_set and next(group_set) then
groups = {};
for group_id in pairs(group_set) do
table.insert(groups, group_id);
end
end
end
return groups;
end
function list_users(event)
local user_list = {};
for username in usermanager.users(module.host) do
table.insert(user_list, get_user_info(username));
end
event.response.headers["Content-Type"] = json_content_type;
return json.encode_array(user_list);
end
function get_user_by_name(event, username)
local property
do
local name, sub_path = username:match("^([^/]+)/(%w+)$");
if name then
username = name;
property = sub_path;
end
end
if property == "groups" then
return json.encode(get_user_groups(username));
end
local user_info = get_user_info(username);
if not user_info then
return 404;
end
event.response.headers["Content-Type"] = json_content_type;
return json.encode(user_info);
end
function delete_user(event, username) --luacheck: ignore 212/event
if not usermanager.delete_user(username, module.host) then
return 404;
end
return 200;
end
function list_groups(event)
local group_list = {};
for group_id in group_store:users() do
table.insert(group_list, {
id = group_id;
name = group_id;
members = group_store:get(group_id);
});
end
event.response.headers["Content-Type"] = json_content_type;
return json.encode_array(group_list);
end
function get_group_by_id(event, group_id)
local group = group_store:get(group_id);
if not group then
return 404;
end
event.response.headers["Content-Type"] = json_content_type;
return json.encode({
id = group_id;
name = group_id;
members = group;
});
end
function create_group(event)
local request = event.request;
if request.headers.content_type ~= json_content_type
or (not request.body or #request.body == 0) then
return 400;
end
local group = json.decode(event.request.body);
if not group then
return 400;
end
local ok = group_store:set(group.id, {});
if not ok then
return 500;
end
event.response.headers["Content-Type"] = json_content_type;
return json.encode({
id = group.id or group.name;
name = group.name;
});
end
function delete_group(event, group_id) --luacheck: ignore 212/event
if not group_id then
return 400;
end
if not group_store:set(group_id, nil) then
return 500;
end
return 200;
end
module:provides("http", {
route = check_auth {
["GET /invites"] = list_invites;
["GET /invites/*"] = get_invite_by_id;
["POST /invites"] = create_invite;
["DELETE /invites/*"] = delete_invite;
["GET /users"] = list_users;
["GET /users/*"] = get_user_by_name;
["DELETE /users/*"] = delete_user;
["GET /groups"] = list_groups;
["GET /groups/*"] = get_group_by_id;
["POST /groups"] = create_group;
["DELETE /groups/*"] = delete_group;
};
});