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