luajitos

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

OrbitManager.lua (19338B)


      1 -- OrbitManager: Secure permissions manager for LuaJIT OS
      2 -- Provides sandboxing and permission-based access control for global functions
      3 
      4 local OrbitManager = {}
      5 
      6 -- Private storage using weak tables to prevent external access
      7 local private = setmetatable({}, {__mode = "k"})
      8 
      9 -- Stub function generator - creates wrapper functions that check permissions
     10 local function createStub(orbitInstance, funcPath, realFunc)
     11     return function(...)
     12         local priv = private[orbitInstance]
     13 
     14         -- Find the permission category for this function
     15         local permCategory = priv.map[funcPath]
     16 
     17         if not permCategory then
     18             error("OrbitManager: Function " .. funcPath .. " is not mapped to a permission category", 2)
     19         end
     20 
     21         -- Check if permission is granted
     22         if not priv.perms[permCategory] then
     23             error("OrbitManager: Attempted to call " .. funcPath .. " without required permissions (needs " .. permCategory .. ")", 2)
     24         end
     25 
     26         -- Special handling for filesystem operations
     27         if permCategory == "perms.fs" then
     28             local path = select(1, ...)
     29             if path and type(path) == "string" then
     30                 if not orbitInstance:checkPathAllowed(path) then
     31                     error("OrbitManager: Attempted to access path '" .. path .. "' which is not in allowedPaths", 2)
     32                 end
     33             end
     34         end
     35 
     36         -- Special handling for network operations
     37         if permCategory == "perms.network" then
     38             local domain = select(1, ...)
     39             if domain and type(domain) == "string" then
     40                 if not orbitInstance:checkDomainAllowed(domain) then
     41                     error("OrbitManager: Attempted to access domain '" .. domain .. "' which is not in allowedDomains", 2)
     42                 end
     43             end
     44         end
     45 
     46         -- Call the real function
     47         return realFunc(...)
     48     end
     49 end
     50 
     51 -- Wildcard path matching helper
     52 local function matchWildcard(pattern, str)
     53     -- Convert wildcard pattern to Lua pattern
     54     -- Escape special Lua pattern characters except *
     55     local luaPattern = pattern:gsub("[%(%)%.%%%+%-%?%[%]%^%$]", "%%%1")
     56     -- Convert * to .*
     57     luaPattern = luaPattern:gsub("%*", ".*")
     58     -- Anchor the pattern
     59     luaPattern = "^" .. luaPattern .. "$"
     60 
     61     return str:match(luaPattern) ~= nil
     62 end
     63 
     64 -- Recursively iterate through a table and replace functions
     65 local function replaceGlobalFunctions(orbitInstance, tbl, priv, path)
     66     path = path or ""
     67 
     68     for key, value in pairs(tbl) do
     69         local fullPath = path == "" and key or (path .. "." .. key)
     70 
     71         if type(value) == "function" then
     72             -- Check if this function is in our map
     73             if priv.map[fullPath] then
     74                 -- Store the real function
     75                 priv.reals[fullPath] = value
     76                 -- Replace with stub
     77                 tbl[key] = createStub(orbitInstance, fullPath, value)
     78             end
     79         elseif type(value) == "table" and key ~= "_G" then
     80             -- Recursively process tables, but avoid infinite loops
     81             replaceGlobalFunctions(orbitInstance, value, priv, fullPath)
     82         end
     83     end
     84 end
     85 
     86 -- Helper function to initialize instance with common setup
     87 local function initializeInstance(target_G, permsData)
     88     local instance = {}
     89 
     90     -- Initialize private storage for this instance
     91     private[instance] = {
     92         map = {
     93             ["io.open"] = "perms.fs",
     94             ["io.read"] = "perms.fs",
     95             ["io.write"] = "perms.fs",
     96             ["io.close"] = "perms.fs",
     97             ["io.lines"] = "perms.fs",
     98             ["io.input"] = "perms.fs",
     99             ["io.output"] = "perms.fs",
    100             ["os.execute"] = "perms.os",
    101             ["os.exit"] = "perms.os",
    102             ["os.remove"] = "perms.fs",
    103             ["os.rename"] = "perms.fs",
    104             ["os.getenv"] = "perms.os",
    105             ["os.clock"] = "perms.os",
    106             ["os.date"] = "perms.os",
    107             ["os.time"] = "perms.os",
    108             ["os.tmpname"] = "perms.fs",
    109         },
    110         perms = {
    111             ["perms.fs"] = false,
    112             ["perms.os"] = false,
    113             ["perms.network"] = false,
    114         },
    115         reals = {},
    116         allowedPaths = {},
    117         allowedDomains = {},
    118         manifestData = {}, -- Store manifest metadata
    119     }
    120 
    121     local priv = private[instance]
    122 
    123     -- Apply permissions data if provided
    124     if permsData and type(permsData) == "table" then
    125         -- Store manifest metadata (name, dev, etc.)
    126         if permsData.name then
    127             priv.manifestData.name = permsData.name
    128         end
    129         if permsData.dev then
    130             priv.manifestData.dev = permsData.dev
    131         end
    132 
    133         -- Update permissions - handle both "perms.fs" and "fs" formats
    134         if permsData.perms then
    135             for perm, value in pairs(permsData.perms) do
    136                 -- Normalize permission names
    137                 local normalizedPerm = perm
    138                 if not perm:match("^perms%.") then
    139                     normalizedPerm = "perms." .. perm
    140                 end
    141                 priv.perms[normalizedPerm] = value
    142             end
    143         end
    144 
    145         -- Update allowed paths
    146         if permsData.allowedPaths then
    147             priv.allowedPaths = permsData.allowedPaths
    148         end
    149 
    150         -- Update allowed domains
    151         if permsData.allowedDomains then
    152             priv.allowedDomains = permsData.allowedDomains
    153         end
    154     end
    155 
    156     -- Replace functions in target_G
    157     if target_G then
    158         replaceGlobalFunctions(instance, target_G, priv, "")
    159     end
    160 
    161     -- Check if a path is allowed
    162     function instance:checkPathAllowed(path)
    163         local priv = private[self]
    164 
    165         -- If no restrictions, deny by default
    166         if not priv.allowedPaths or #priv.allowedPaths == 0 then
    167             return false
    168         end
    169 
    170         -- Check against all allowed paths
    171         for _, allowedPath in ipairs(priv.allowedPaths) do
    172             if matchWildcard(allowedPath, path) then
    173                 return true
    174             end
    175         end
    176 
    177         return false
    178     end
    179 
    180     -- Check if a domain is allowed
    181     function instance:checkDomainAllowed(domain)
    182         local priv = private[self]
    183 
    184         -- If no restrictions, deny by default
    185         if not priv.allowedDomains or #priv.allowedDomains == 0 then
    186             return false
    187         end
    188 
    189         -- Check against all allowed domains
    190         for _, allowedDomain in ipairs(priv.allowedDomains) do
    191             if matchWildcard(allowedDomain, domain) then
    192                 return true
    193             end
    194         end
    195 
    196         return false
    197     end
    198 
    199     -- Check if a function can be run with optional path/domain
    200     function instance:canRun(funcPath, pathOrDomain)
    201         local priv = private[self]
    202 
    203         -- Find the permission category
    204         local permCategory = priv.map[funcPath]
    205 
    206         if not permCategory then
    207             return false, "Function not mapped to any permission category"
    208         end
    209 
    210         -- Check if permission is granted
    211         if not priv.perms[permCategory] then
    212             return false, "Permission " .. permCategory .. " not granted"
    213         end
    214 
    215         -- Additional checks for filesystem and network
    216         if pathOrDomain then
    217             if permCategory == "perms.fs" then
    218                 if not self:checkPathAllowed(pathOrDomain) then
    219                     return false, "Path not in allowedPaths"
    220                 end
    221             elseif permCategory == "perms.network" then
    222                 if not self:checkDomainAllowed(pathOrDomain) then
    223                     return false, "Domain not in allowedDomains"
    224                 end
    225             end
    226         end
    227 
    228         return true
    229     end
    230 
    231     -- Grant a permission
    232     function instance:grantPermission(permCategory)
    233         local priv = private[self]
    234         -- Normalize permission name
    235         if not permCategory:match("^perms%.") then
    236             permCategory = "perms." .. permCategory
    237         end
    238         priv.perms[permCategory] = true
    239     end
    240 
    241     -- Revoke a permission
    242     function instance:revokePermission(permCategory)
    243         local priv = private[self]
    244         -- Normalize permission name
    245         if not permCategory:match("^perms%.") then
    246             permCategory = "perms." .. permCategory
    247         end
    248         priv.perms[permCategory] = false
    249     end
    250 
    251     -- Add an allowed path
    252     function instance:addAllowedPath(path)
    253         local priv = private[self]
    254         table.insert(priv.allowedPaths, path)
    255     end
    256 
    257     -- Add an allowed domain
    258     function instance:addAllowedDomain(domain)
    259         local priv = private[self]
    260         table.insert(priv.allowedDomains, domain)
    261     end
    262 
    263     -- Get manifest metadata
    264     function instance:getManifestData()
    265         local priv = private[self]
    266         -- Return a copy to prevent modification
    267         local copy = {}
    268         for k, v in pairs(priv.manifestData) do
    269             copy[k] = v
    270         end
    271         return copy
    272     end
    273 
    274     -- Protected metatable - prevents access to internal structure
    275     setmetatable(instance, {
    276         __index = function(t, k)
    277             if k == "map" or k == "perms" or k == "reals" or k == "manifestData" then
    278                 error("OrbitManager: Direct access to '" .. k .. "' is not allowed", 2)
    279             end
    280             return rawget(t, k)
    281         end,
    282         __newindex = function(t, k, v)
    283             if k == "map" or k == "perms" or k == "reals" or k == "manifestData" then
    284                 error("OrbitManager: Direct modification of '" .. k .. "' is not allowed", 2)
    285             end
    286             rawset(t, k, v)
    287         end,
    288         __metatable = false, -- Hide the metatable
    289     })
    290 
    291     return instance
    292 end
    293 
    294 -- Create a new OrbitManager instance from a permissions file
    295 function OrbitManager.new(target_G, permissionsFile)
    296     permissionsFile = permissionsFile or "perms.lua"
    297 
    298     local permsData = nil
    299 
    300     -- Load permissions from file
    301     local permsChunk, err = loadfile(permissionsFile)
    302     if permsChunk then
    303         permsData = permsChunk()
    304     end
    305 
    306     return initializeInstance(target_G, permsData)
    307 end
    308 
    309 -- Create a new OrbitManager instance from a manifest string
    310 function OrbitManager.newFromManifest(target_G, manifestString)
    311     if type(manifestString) ~= "string" then
    312         error("OrbitManager.newFromManifest: manifestString must be a string", 2)
    313     end
    314 
    315     -- Load and execute the manifest string
    316     local manifestChunk, err = load(manifestString, "manifest", "t")
    317     if not manifestChunk then
    318         error("OrbitManager.newFromManifest: Failed to parse manifest: " .. tostring(err), 2)
    319     end
    320 
    321     local success, manifestData = pcall(manifestChunk)
    322     if not success then
    323         error("OrbitManager.newFromManifest: Failed to execute manifest: " .. tostring(manifestData), 2)
    324     end
    325 
    326     if type(manifestData) ~= "table" then
    327         error("OrbitManager.newFromManifest: Manifest must return a table", 2)
    328     end
    329 
    330     return initializeInstance(target_G, manifestData)
    331 end
    332 
    333 -- Deep copy a table recursively
    334 local function deepCopy(orig, copies)
    335     copies = copies or {}
    336     local orig_type = type(orig)
    337     local copy
    338     if orig_type == 'table' then
    339         if copies[orig] then
    340             copy = copies[orig]
    341         else
    342             copy = {}
    343             copies[orig] = copy
    344             for orig_key, orig_value in next, orig, nil do
    345                 copy[deepCopy(orig_key, copies)] = deepCopy(orig_value, copies)
    346             end
    347             setmetatable(copy, deepCopy(getmetatable(orig), copies))
    348         end
    349     else
    350         copy = orig
    351     end
    352     return copy
    353 end
    354 
    355 -- Recursively build sandbox from whitelist
    356 local function buildSandboxFromWhitelist(whitelist, sourceEnv, orbitInstance, path)
    357     path = path or ""
    358     local sandbox = {}
    359 
    360     for key, value in pairs(whitelist) do
    361         local fullPath = path == "" and key or (path .. "." .. key)
    362         local sourceValue = sourceEnv[key]
    363 
    364         if value == true then
    365             -- Whitelisted - copy directly from source
    366             if sourceValue ~= nil then
    367                 if type(sourceValue) == "table" then
    368                     sandbox[key] = deepCopy(sourceValue)
    369                 else
    370                     sandbox[key] = sourceValue
    371                 end
    372             end
    373         elseif type(value) == "string" then
    374             -- Permission-controlled function
    375             if type(sourceValue) == "function" then
    376                 local priv = private[orbitInstance]
    377                 priv.reals[fullPath] = sourceValue
    378                 priv.map[fullPath] = value
    379                 sandbox[key] = createStub(orbitInstance, fullPath, sourceValue)
    380             elseif sourceValue ~= nil then
    381                 sandbox[key] = sourceValue
    382             end
    383         elseif type(value) == "table" then
    384             -- Nested table - recurse
    385             if type(sourceValue) == "table" then
    386                 sandbox[key] = buildSandboxFromWhitelist(value, sourceValue, orbitInstance, fullPath)
    387             else
    388                 -- Source doesn't have this table, create empty
    389                 sandbox[key] = buildSandboxFromWhitelist(value, {}, orbitInstance, fullPath)
    390             end
    391         end
    392         -- If value is false or nil, the function is blocked (not added to sandbox)
    393     end
    394 
    395     return sandbox
    396 end
    397 
    398 -- Create a new sandbox environment with OrbitManager from a manifest string
    399 function OrbitManager.newSandbox(manifestString, sandboxEnvPath)
    400     if type(manifestString) ~= "string" then
    401         error("OrbitManager.newSandbox: manifestString must be a string", 2)
    402     end
    403 
    404     sandboxEnvPath = sandboxEnvPath or "/home/b/Programming/LuajitOS/OS/sandboxEnv.lua"
    405 
    406     -- Load the manifest
    407     local manifestChunk, err = load(manifestString, "manifest", "t")
    408     if not manifestChunk then
    409         error("OrbitManager.newSandbox: Failed to parse manifest: " .. tostring(err), 2)
    410     end
    411 
    412     local success, manifestData = pcall(manifestChunk)
    413     if not success then
    414         error("OrbitManager.newSandbox: Failed to execute manifest: " .. tostring(manifestData), 2)
    415     end
    416 
    417     if type(manifestData) ~= "table" then
    418         error("OrbitManager.newSandbox: Manifest must return a table", 2)
    419     end
    420 
    421     -- Load the sandbox whitelist
    422     local whitelistChunk, err = loadfile(sandboxEnvPath)
    423     if not whitelistChunk then
    424         error("OrbitManager.newSandbox: Failed to load sandboxEnv.lua: " .. tostring(err), 2)
    425     end
    426 
    427     local whitelist = whitelistChunk()
    428     if type(whitelist) ~= "table" then
    429         error("OrbitManager.newSandbox: sandboxEnv.lua must return a table", 2)
    430     end
    431 
    432     -- Create an OrbitManager instance (without target_G for now)
    433     local instance = {}
    434 
    435     -- Initialize private storage
    436     private[instance] = {
    437         map = {},
    438         perms = {
    439             ["perms.fs"] = false,
    440             ["perms.os"] = false,
    441             ["perms.network"] = false,
    442             ["perms.modules"] = false,
    443         },
    444         reals = {},
    445         allowedPaths = {},
    446         allowedDomains = {},
    447         manifestData = {},
    448     }
    449 
    450     local priv = private[instance]
    451 
    452     -- Apply manifest data
    453     if manifestData.name then
    454         priv.manifestData.name = manifestData.name
    455     end
    456     if manifestData.dev then
    457         priv.manifestData.dev = manifestData.dev
    458     end
    459 
    460     if manifestData.perms then
    461         for perm, value in pairs(manifestData.perms) do
    462             local normalizedPerm = perm
    463             if not perm:match("^perms%.") then
    464                 normalizedPerm = "perms." .. perm
    465             end
    466             priv.perms[normalizedPerm] = value
    467         end
    468     end
    469 
    470     if manifestData.allowedPaths then
    471         priv.allowedPaths = manifestData.allowedPaths
    472     end
    473 
    474     if manifestData.allowedDomains then
    475         priv.allowedDomains = manifestData.allowedDomains
    476     end
    477 
    478     -- Build the sandbox from the whitelist
    479     local sandbox = buildSandboxFromWhitelist(whitelist, _G, instance, "")
    480 
    481     -- Add the _G self-reference
    482     sandbox._G = sandbox
    483 
    484     -- Add instance methods
    485     function instance:checkPathAllowed(path)
    486         local priv = private[self]
    487         if not priv.allowedPaths or #priv.allowedPaths == 0 then
    488             return false
    489         end
    490         for _, allowedPath in ipairs(priv.allowedPaths) do
    491             if matchWildcard(allowedPath, path) then
    492                 return true
    493             end
    494         end
    495         return false
    496     end
    497 
    498     function instance:checkDomainAllowed(domain)
    499         local priv = private[self]
    500         if not priv.allowedDomains or #priv.allowedDomains == 0 then
    501             return false
    502         end
    503         for _, allowedDomain in ipairs(priv.allowedDomains) do
    504             if matchWildcard(allowedDomain, domain) then
    505                 return true
    506             end
    507         end
    508         return false
    509     end
    510 
    511     function instance:canRun(funcPath, pathOrDomain)
    512         local priv = private[self]
    513         local permCategory = priv.map[funcPath]
    514         if not permCategory then
    515             return false, "Function not mapped to any permission category"
    516         end
    517         if not priv.perms[permCategory] then
    518             return false, "Permission " .. permCategory .. " not granted"
    519         end
    520         if pathOrDomain then
    521             if permCategory == "perms.fs" then
    522                 if not self:checkPathAllowed(pathOrDomain) then
    523                     return false, "Path not in allowedPaths"
    524                 end
    525             elseif permCategory == "perms.network" then
    526                 if not self:checkDomainAllowed(pathOrDomain) then
    527                     return false, "Domain not in allowedDomains"
    528                 end
    529             end
    530         end
    531         return true
    532     end
    533 
    534     function instance:grantPermission(permCategory)
    535         local priv = private[self]
    536         if not permCategory:match("^perms%.") then
    537             permCategory = "perms." .. permCategory
    538         end
    539         priv.perms[permCategory] = true
    540     end
    541 
    542     function instance:revokePermission(permCategory)
    543         local priv = private[self]
    544         if not permCategory:match("^perms%.") then
    545             permCategory = "perms." .. permCategory
    546         end
    547         priv.perms[permCategory] = false
    548     end
    549 
    550     function instance:addAllowedPath(path)
    551         local priv = private[self]
    552         table.insert(priv.allowedPaths, path)
    553     end
    554 
    555     function instance:addAllowedDomain(domain)
    556         local priv = private[self]
    557         table.insert(priv.allowedDomains, domain)
    558     end
    559 
    560     function instance:getManifestData()
    561         local priv = private[self]
    562         local copy = {}
    563         for k, v in pairs(priv.manifestData) do
    564             copy[k] = v
    565         end
    566         return copy
    567     end
    568 
    569     function instance:getSandbox()
    570         return sandbox
    571     end
    572 
    573     -- Protected metatable
    574     setmetatable(instance, {
    575         __index = function(t, k)
    576             if k == "map" or k == "perms" or k == "reals" or k == "manifestData" then
    577                 error("OrbitManager: Direct access to '" .. k .. "' is not allowed", 2)
    578             end
    579             return rawget(t, k)
    580         end,
    581         __newindex = function(t, k, v)
    582             if k == "map" or k == "perms" or k == "reals" or k == "manifestData" then
    583                 error("OrbitManager: Direct modification of '" .. k .. "' is not allowed", 2)
    584             end
    585             rawset(t, k, v)
    586         end,
    587         __metatable = false,
    588     })
    589 
    590     return instance, sandbox
    591 end
    592 
    593 return OrbitManager