diff --git a/modules/network/constants.lua b/modules/network/constants.lua new file mode 100644 index 0000000..5e53c8d --- /dev/null +++ b/modules/network/constants.lua @@ -0,0 +1,57 @@ +local me = microexpansion +if not me.constants then + me.constants = {} +end +local constants = me.constants + +local access_levels = { + -- cannot interact at all with the network or it's components + blocked = 0, + -- can only look into the network but not move, modify, etc. + view = 20, + -- can use chests, craft terminals, etc. + interact = 40, + -- can use all components except security, can build and dig (except core) + modify = 60, + -- can use security terminal, can modify all players with less access + manage = 80, + -- can modify all players with less access and self + full = 100 +} + +local access_level_descriptions = {} +access_level_descriptions[access_levels.blocked] = { + name = "Blocked", + color = "gray", + index = 1 +} +access_level_descriptions[access_levels.view] = { + name = "View", + color = "orange", + index = 2 +} +access_level_descriptions[access_levels.interact] = { + color = "yellow", + name = "Interact", + index = 3 +} +access_level_descriptions[access_levels.modify] = { + name = "Modify", + color = "yellowgreen", + index = 4 +} +access_level_descriptions[access_levels.manage] = { + name = "Manage", + color = "green", + index = 5 +} +access_level_descriptions[access_levels.full] = { + name = "Full", + color = "blue", + index = 6 +} + +constants.security = { + access_levels = access_levels, + access_level_descriptions = access_level_descriptions +} diff --git a/modules/network/ctrl.lua b/modules/network/ctrl.lua index 58d783e..e055b7c 100644 --- a/modules/network/ctrl.lua +++ b/modules/network/ctrl.lua @@ -2,8 +2,7 @@ local me = microexpansion local network = me.network - ---FIXME: accept multiple controllers in one network +local access_level = microexpansion.constants.security.access_levels -- [register node] Controller me.register_node("ctrl", { @@ -45,6 +44,10 @@ me.register_node("ctrl", { groups = { cracky = 1, me_connect = 1, }, connect_sides = "nobottom", me_update = function(pos,_,ev) + local meta = minetest.get_meta(pos) + if meta:get_string("source") ~= "" then + return + end local cnet = me.get_network(pos) if cnet == nil then microexpansion.log("no network for ctrl at pos "..minetest.pos_to_string(pos),"error") @@ -54,30 +57,70 @@ me.register_node("ctrl", { end, on_construct = function(pos) local meta = minetest.get_meta(pos) - local net = network.new({controller_pos = pos}) - table.insert(me.networks,net) - me.send_event(pos,"connect",{net=net}) - + local net,cp = me.get_connected_network(pos) + if net then + meta:set_string("source", vector.to_string(cp)) + else + net = network.new({controller_pos = pos}) + table.insert(me.networks,net) + end + me.send_event(pos,"connect",{net=net}) meta:set_string("infotext", "Network Controller") end, after_place_node = function(pos, player) local name = player:get_player_name() local meta = minetest.get_meta(pos) - meta:set_string("infotext", "Network Controller (owned by "..name..")") + meta:set_string("infotext", "Network Controller") meta:set_string("owner", name) + local net,idx = me.get_network(pos) + if net then + net:set_access_level(name, me.constants.security.access_levels.full) + elseif meta:get_string("source") == "" then + me.log("no network after placing controller", "warning") + end + end, + can_dig = function(pos, player) + if not player then + return false + end + local name = player:get_player_name() + if minetest.is_protected(pos, name) then + minetest.record_protection_violation(pos, name) + return false + end + local meta = minetest.get_meta(pos) + local net + if meta:get_string("source") == "" then + net = me.get_network(pos) + else + net = me.get_connected_network(pos) + end + if not net then + me.log("ME Network Controller without Network","error") + return false + end + return net:get_access_level(name) >= access_level.full end, on_destruct = function(pos) local net,idx = me.get_network(pos) --disconnect all those who need the network me.send_event(pos,"disconnect",{net=net}) if net then - net:destruct() + if me.promote_controller(pos,net) then + --reconnect with new controller + me.send_event(pos,"reconnect",{net=net}) + else + net:destruct() + if idx then + table.remove(me.networks,idx) + end + --disconnect all those that haven't realized the network is gone + me.send_event(pos,"disconnect") + end + else + -- disconnect just in case + me.send_event(pos,"disconnect") end - if idx then - table.remove(me.networks,idx) - end - --disconnect all those that haven't realized the network is gone - me.send_event(pos,"disconnect") end, after_destruct = function(pos) --disconnect all those that haven't realized the controller was disconnected @@ -114,11 +157,48 @@ me.register_machine("cable", { paramtype = "light", groups = { crumbly = 1, }, --TODO: move these functions into the registration + can_dig = function(pos, player) + if not player then + return false + end + local name = player:get_player_name() + if minetest.is_protected(pos, name) then + minetest.record_protection_violation(pos, name) + return false + end + local net,cp = me.get_connected_network(pos) + if not net then + return true + end + return net:get_access_level(name) >= access_level.modify + end, on_construct = function(pos) me.send_event(pos,"connect") end, + after_place_node = function(pos, placer) + if not placer then + return false + end + local name = placer:get_player_name() + if minetest.is_protected(pos, name) then + minetest.record_protection_violation(pos, name) + --protection probably handles this itself + --minetest.remove_node(pos) + return true + end + local net,cp = me.get_connected_network(pos) + if not net then + return false + end + if net:get_access_level(name) < access_level.modify then + -- prevent placing cables on a network that a player doesn't have access to + minetest.remove_node(pos) + return true + end + end, after_destruct = function(pos) - me.send_event(pos,"disconnect") + --FIXME: write drives before disconnecting + me.send_event(pos,"disconnect") end, me_update = function(pos,_,ev) if ev then diff --git a/modules/network/init.lua b/modules/network/init.lua index 779367e..6b56e32 100644 --- a/modules/network/init.lua +++ b/modules/network/init.lua @@ -3,6 +3,8 @@ me.networks = {} local networks = me.networks local path = microexpansion.get_module_path("network") +dofile(path.."/constants.lua") + --deprecated: use ItemStack(x) instead --[[ local function split_stack_values(stack) @@ -110,7 +112,13 @@ end function me.get_connected_network(start_pos) for npos,nn in me.connected_nodes(start_pos,true) do if nn == "microexpansion:ctrl" then - local network = me.get_network(npos) + local source = minetest.get_meta(npos):get_string("source") + local network + if source == "" then + network = me.get_network(npos) + else + network = me.get_network(vector.from_string(source)) + end if network then return network,npos end @@ -118,6 +126,22 @@ function me.get_connected_network(start_pos) end end +function me.promote_controller(start_pos,net) + local promoted = false + for npos,nn in me.connected_nodes(start_pos,true) do + if nn == "microexpansion:ctrl" and npos ~= start_pos then + if promoted then + minetest.get_meta(npos):set_string("source", promoted) + else + promoted = vector.to_string(npos) + minetest.get_meta(npos):set_string("source", "") + net.controller_pos = npos + end + end + end + return promoted and true or false +end + function me.update_connected_machines(start_pos,event,include_start) microexpansion.log("updating connected machines","action") local ev = event or {type = "n/a"} @@ -158,6 +182,7 @@ function me.get_network(pos) end dofile(path.."/ctrl.lua") -- Controller/wires +dofile(path.."/security.lua") --Security Terminal -- load networks function me.load() diff --git a/modules/network/network.lua b/modules/network/network.lua index 0b0340e..0f4cbe1 100644 --- a/modules/network/network.lua +++ b/modules/network/network.lua @@ -1,15 +1,20 @@ --- Microexpansion network -- @type network -- @field #table controller_pos the position of the controller +-- @field #table access a table of players and their respective access levels +-- @field #number default_access_level the access level of unlisted players -- @field #number power_load the power currently provided to the network -- @field #number power_storage the power that can be stored for the next tick local network = { + default_access_level = microexpansion.constants.security.access_levels.view, power_load = 0, power_storage = 0 } local me = microexpansion me.network = network +local access_level = microexpansion.constants.security.access_levels + --- construct a new network -- @function [parent=#network] new -- @param #table o the object to become a network or nil @@ -72,6 +77,65 @@ function network.adjacent_connected_nodes(pos, include_ctrl) return nodes end +function network:get_access_level(player) + local name + if not player then + return self.default_access_level + elseif type(player) == "string" then + name = player + else + name = player:get_player_name() + end + if not self.access then + return self.default_access_level + end + return self.access[name] or self.default_access_level +end + +function network:set_access_level(player, level) + local name + if not player then + self.default_access_level = level + elseif type(player) == "string" then + name = player + else + name = player:get_player_name() + end + if not self.access then + self.access = {} + end + self.access[name] = level + self:fallback_access() +end + +function network:fallback_access() + local full_access = access_level.full + if not self.access then + --something must have gone badly wrong + me.log("no network access table in fallback method","error") + self.access = {} + end + for _,l in pairs(self.access) do + if l == full_access then + return + end + end + local meta = minetest.get_meta(self.controller_pos) + local owner = meta:get_string("owner") + if owner == "" then + me.log("ME Network Controller without owner at: " .. vector.to_string(self.controller_pos), "warning") + else + self.access[owner] = full_access + end +end + +function network:list_access() + if not self.access then + self.access = {} + end + return self.access +end + --- provide power to the network -- @function [parent=#network] provide -- @param #number power the amount of power provided @@ -205,7 +269,10 @@ end local function create_inventory(net) local invname = net:get_inventory_name() net.inv = minetest.create_detached_inventory(invname, { - allow_put = function(inv, listname, index, stack) + allow_put = function(inv, listname, index, stack, player) + if net:get_access_level(player) < access_level.interact then + return 0 + end local inside_stack = inv:get_stack(listname, index) local stack_name = stack:get_name() if minetest.get_item_group(stack_name, "microexpansion_cell") > 0 then @@ -238,7 +305,10 @@ local function create_inventory(net) me.insert_item(stack, inv, listname) net:set_storage_space(true) end, - allow_take = function(_, _, _, stack) + allow_take = function(_, _, _, stack, player) + if net:get_access_level(player) < access_level.interact then + return 0 + end return math.min(stack:get_count(),stack:get_stack_max()) end, on_take = function() diff --git a/modules/network/security.lua b/modules/network/security.lua new file mode 100644 index 0000000..b980eab --- /dev/null +++ b/modules/network/security.lua @@ -0,0 +1,275 @@ +-- microexpansion/network/security.lua + +local me = microexpansion +local access_level = me.constants.security.access_levels +local access_desc = me.constants.security.access_level_descriptions + +-- [me security] Get formspec +local function security_formspec(pos, player, rule, q) + local list + local buttons + local logout = true + local query = q or "" + local net,cp = me.get_connected_network(pos) + if player and cp then + local access = net:get_access_level(player) + if access < access_level.manage then -- Blocked dialog + logout = false + list = "label[2.5,3;"..minetest.colorize("red", "Access Denied").."]" + buttons = "button[3.5,6;2,1;logout;back]" + elseif (not rule) or rule == "" then -- Main Screen + --TODO: show button or entry for default access level + list = "tablecolumns[color,span=1;text;color,span=1;text]" + .. "table[0.5,2;6,7;access_table;" + local first = true + --TODO: filter + local name_list = {} + for p,l in pairs(net:list_access()) do + if first then + first = false + else + list = list .. "," + end + table.insert(name_list, p) + local desc = access_desc[l] or {name = "Unknown", color = "red"} + list = list .. "cyan," .. p .. "," .. desc.color .. "," .. desc.name + end + list = list .. ";]" + minetest.get_meta(pos):set_string("table_index", minetest.serialize(name_list)) + + list = list .. [[ + field[0.5,1;2,0.5;filter;;]]..query..[[] + button[3,1;0.8,0.5;search;?] + button[4,1;0.8,0.5;clear;X] + tooltip[search;Search] + tooltip[clear;Reset] + field_close_on_enter[filter;false] + ]] + buttons = [[ + button[7,7;1.5,0.8;new;new rule] + button[7,8;1.5,0.8;edit_sel;edit rule] + ]] + --button[7,6;1.5,0.8;del_sel;delete rule] + elseif rule == "" then -- Creation screen + logout = false + local players = "" + for _,p in pairs(minetest.get_connected_players()) do + if players ~= "" then + players = players .. "," + end + players = players .. p:get_player_name() + end + --TODO: add a text field (maybe toggelable) + list = [[ + dropdown[3,2.75;5,0.5;new_player;]]..players..[[;] + label[1.5,3;rule for:] + ]] + buttons = [[ + button[2,6;2,0.8;edit;add/edit] + button[5,6;2,0.8;back;cancel] + ]] + elseif (access < access_level.full and net:get_access_level(rule) >= access_level.manage) or (player ~= rule and access >= access_level.full and net:get_access_level(rule) >= access_level.full) then + -- low access dialog + list = "label[1,3;"..minetest.colorize("red", "You can only modify rules with lower access than yourself.").."]" + buttons = "button[3.5,6;2,1;back;back]" + else + local rule_level = net:get_access_level(rule) + local current = rule_level == access_level.blocked and "1" or + rule_level == access_level.view and "2" or + rule_level == access_level.interact and "3" or + rule_level == access_level.modify and "4" or + rule_level == access_level.manage and "5" or + rule_level == access_level.full and "6" or "" + list = [[ + label[1,3;rule for:]].."\t"..minetest.colorize("cyan", rule)..[[] + label[1,4;access level:] + dropdown[3,3.75;2,0.5;access;Blocked,View,Interact,Modify,Manage,Full;]]..current..[[] + ]] + buttons = [[ + button[1,6;1,0.8;save;save] + button[3,6;2,0.8;reset;reset to default] + button[6,6;1,0.8;back;cancel] + ]] + end + elseif cp then + logout = false + list = "label[2.5,3;Welcome to the security Terminal!]" + buttons = [[ + button[3.5,6;2,1;login;login] + button_exit[8,0.5;0.5,0.5;close;x] + ]] + else + logout = false + list = "label[2.5,3;" .. minetest.colorize("red", "No connected network!") .. "]" + buttons = "button_exit[3,6;2,1;close;close]" + end + + return [[ + formspec_version[2] + size[9,9.5] + ]].. + microexpansion.gui_bg .. + list .. + (logout and "button[7.5,0.5;1,0.5;logout;logout]" or "") .. + "label[0.5,0.5;ME Security Terminal]" .. + buttons +end + +local function update_security(pos,_,ev) + --for now all events matter + + local network = me.get_connected_network(pos) + local meta = minetest.get_meta(pos) + if network == nil then + meta:set_string("editing_rule", "") + meta:set_string("formspec", security_formspec(pos)) + end + meta:set_string("formspec", security_formspec(pos)) +end + +-- [me chest] Register node +microexpansion.register_node("security", { + description = "ME Security Terminal", + usedfor = "Allows controlling access to ME networks", + tiles = { + "security_bottom", + "security_bottom", + "chest_side", + "chest_side", + "chest_side", + "security_front", + }, + recipe = { + { 1, { + {"default:steel_ingot", "default:copper_ingot", "default:steel_ingot"}, + {"default:steel_ingot", "microexpansion:machine_casing", "default:steel_ingot"}, + {"default:steel_ingot", "microexpansion:cable", "default:steel_ingot"}, + }, + } + }, + is_ground_content = false, + groups = { cracky = 1, me_connect = 1 }, + paramtype = "light", + paramtype2 = "facedir", + me_update = update_security, + on_construct = function(pos) + local meta = minetest.get_meta(pos) + local net = me.get_connected_network(pos) + me.send_event(pos,"connect",{net=net}) + update_security(pos) + end, + after_destruct = function(pos) + me.send_event(pos,"disconnect") + end, + can_dig = function(pos, player) + if not player then + return false + end + local name = player:get_player_name() + if minetest.is_protected(pos, name) then + minetest.record_protection_violation(pos, name) + return false + end + local net,cp = me.get_connected_network(pos) + if not net then + return true + end + return net:get_access_level(name) >= access_level.manage + end, + on_receive_fields = function(pos, _, fields, sender) + if fields.close then + return + end + local net,cp = me.get_connected_network(pos) + if net then + if cp then + microexpansion.log("network and ctrl_pos","info") + else + microexpansion.log("network but no ctrl_pos","warning") + end + else + if cp then + microexpansion.log("no network but ctrl_pos","warning") + else + microexpansion.log("no network and no ctrl_pos","info") + end + end + local meta = minetest.get_meta(pos) + local name = sender:get_player_name() + if not net then + microexpansion.log("no network connected to security terminal","warning") + return + end + if fields.logout then + meta:set_string("formspec", security_formspec(pos)) + elseif fields.login or fields.back then + meta:set_string("formspec", security_formspec(pos, name)) + elseif fields.search or fields.key_enter_field == "filter" then + meta:set_string("formspec", security_formspec(pos, name), false, fields.filter) + elseif fields.clear then + meta:set_string("formspec", security_formspec(pos, name)) + elseif fields.new then + meta:set_string("formspec", security_formspec(pos, name, "")) + elseif fields.edit then + local access = net:get_access_level(name) + if not fields.new_player then + me.log("edit button without new player field","warning") + meta:set_string("formspec", security_formspec(pos, name)) + return + end + if net:get_access_level(fields.new_player) == nil then + if access >= access_level.manage then + net:set_access_level(fields.new_player, net:get_access_level()) + end + end + meta:set_string("editing_rule", fields.new_player) + meta:set_string("formspec", security_formspec(pos, name, fields.new_player)) + elseif fields.edit_sel then + meta:set_string("formspec", security_formspec(pos, name, meta:get_string("editing_rule"))) + elseif fields.access_table then + local ev = minetest.explode_table_event(fields.access_table) + local table_index = minetest.deserialize(meta:get_string("table_index")) + local edit_player = table_index[ev.row] + if net:get_access_level(edit_player) == nil then + me.log("playerlist changed before editing","warning") + meta:set_string("formspec", security_formspec(pos, name)) + return + else + meta:set_string("editing_rule", edit_player) + if ev.type == "DCL" then + meta:set_string("formspec", security_formspec(pos, name, edit_player)) + end + end + elseif fields.reset then + local rule = meta:get_string("editing_rule") + local access = net:get_access_level(name) + local old_level = net:get_access_level(rule) + local new_level = net.default_access_level + if (access > old_level or name == rule) and (access > new_level or access >= access_level.full) then + net:set_access_level(rule, nil) + --TODO: show fail dialog if access violation + end + meta:set_string("formspec", security_formspec(pos, name)) + elseif fields.save then + local rule = meta:get_string("editing_rule") + local access = net:get_access_level(name) + local old_level = net:get_access_level(rule) + local new_level = fields.access == "Blocked" and access_level.blocked or + fields.access == "View" and access_level.view or + fields.access == "Interact" and access_level.interact or + fields.access == "Modify" and access_level.modify or + fields.access == "Manage" and access_level.manage or + fields.access == "Full" and access_level.full + if not new_level then + me.log("unknown access level selection " .. fields.access, "error") + --TODO: show fail dialog + return + end + if (access > old_level or name == rule) and access > new_level then + net:set_access_level(rule, new_level) + --TODO: show fail dialog if access violation + end + meta:set_string("formspec", security_formspec(pos, name)) + end + end, +}) diff --git a/modules/storage/drive.lua b/modules/storage/drive.lua index 458285a..ea09fe5 100644 --- a/modules/storage/drive.lua +++ b/modules/storage/drive.lua @@ -1,6 +1,7 @@ -- microexpansion/machines.lua local me = microexpansion +local access_level = microexpansion.constants.security.access_levels local netdrives @@ -312,9 +313,21 @@ microexpansion.register_node("drive", { me.send_event(pos,"connect") end, can_dig = function(pos, player) - if minetest.is_protected(pos, player) then - return false - end + if not player then + return false + end + local name = player:get_player_name() + if minetest.is_protected(pos, name) then + minetest.record_protection_violation(pos, name) + return false + end + local net,cp = me.get_connected_network(pos) + if not net then + return true + end + if net:get_access_level(name) < access_level.modify then + return false + end local meta = minetest.get_meta(pos) local inv = meta:get_inventory() return inv:is_empty("main") @@ -322,8 +335,17 @@ microexpansion.register_node("drive", { after_destruct = function(pos) me.send_event(pos,"disconnect") end, - allow_metadata_inventory_put = function(_, _, _, stack) - if minetest.is_protected(pos, player) or minetest.get_item_group(stack:get_name(), "microexpansion_cell") == 0 then + allow_metadata_inventory_put = function(pos, _, _, stack, player) + local network = me.get_connected_network(pos) + if network then + if network:get_access_level(player) < access_level.interact then + return 0 + end + elseif minetest.is_protected(pos, player) then + minetest.record_protection_violation(pos, player) + return 0 + end + if minetest.get_item_group(stack:get_name(), "microexpansion_cell") == 0 then return 0 else return 1 @@ -349,11 +371,16 @@ microexpansion.register_node("drive", { me.send_event(pos,"items",{net=network}) end, allow_metadata_inventory_take = function(pos,_,_,stack, player) --args: pos, listname, index, stack, player - if minetest.is_protected(pos, player) then - return 0 - end - local network = me.get_connected_network(pos) - write_drive_cells(pos,network) + local network = me.get_connected_network(pos) + if network then + write_drive_cells(pos,network) + if network:get_access_level(player) < access_level.interact then + return 0 + end + elseif minetest.is_protected(pos, player) then + minetest.record_protection_violation(pos, player) + return 0 + end return stack:get_count() end, on_metadata_inventory_take = function(pos, _, _, stack) diff --git a/modules/storage/terminal.lua b/modules/storage/terminal.lua index 731c3f8..a678998 100644 --- a/modules/storage/terminal.lua +++ b/modules/storage/terminal.lua @@ -2,6 +2,7 @@ local me = microexpansion local pipeworks_enabled = minetest.get_modpath("pipeworks") and true or false +local access_level = microexpansion.constants.security.access_levels -- [me chest] Get formspec local function chest_formspec(pos, start_id, listname, page_max, q) @@ -121,14 +122,53 @@ microexpansion.register_node("term", { update_chest(pos) end end, + can_dig = function(pos, player) + if not player then + return false + end + local name = player:get_player_name() + if minetest.is_protected(pos, name) then + minetest.record_protection_violation(pos, name) + return false + end + local net,cp = me.get_connected_network(pos) + if not net then + return true + end + return net:get_access_level(name) >= access_level.modify + end, after_destruct = function(pos) me.send_event(pos,"disconnect") end, + allow_metadata_inventory_put = function(pos, _, _, stack, player) + local network = me.get_connected_network(pos) + if network then + if network:get_access_level(player) < access_level.interact then + return 0 + end + elseif minetest.is_protected(pos, player) then + minetest.record_protection_violation(pos, player) + return 0 + end + return stack:get_count() + end, on_metadata_inventory_put = function(pos, listname, _, stack) local net = me.get_connected_network(pos) local inv = net:get_inventory() me.insert_item(stack, inv, "main") end, + allow_metadata_inventory_take = function(pos,_,_,stack, player) + local network = me.get_connected_network(pos) + if network then + if network:get_access_level(player) < access_level.interact then + return 0 + end + elseif minetest.is_protected(pos, player) then + minetest.record_protection_violation(pos, player) + return 0 + end + return math.min(stack:get_count(),stack:get_stack_max()) + end, on_metadata_inventory_take = function(pos, listname, _, stack) local net = me.get_connected_network(pos) local inv = net:get_inventory() @@ -242,6 +282,9 @@ microexpansion.register_node("term", { meta:set_string("inv_name", "main") meta:set_string("formspec", chest_formspec(pos, 1, "main", page_max)) elseif fields.tochest then + if net:get_access_level(sender) < access_level.interact then + return + end local pinv = minetest.get_inventory({type="player", name=sender:get_player_name()}) net:set_storage_space(pinv:get_size("main")) local space = net:get_item_capacity() diff --git a/textures/microexpansion_security_bottom.png b/textures/microexpansion_security_bottom.png new file mode 100644 index 0000000..4694331 Binary files /dev/null and b/textures/microexpansion_security_bottom.png differ diff --git a/textures/microexpansion_security_front.png b/textures/microexpansion_security_front.png new file mode 100644 index 0000000..29449ec Binary files /dev/null and b/textures/microexpansion_security_front.png differ