{"id":21387168,"url":"https://github.com/sshtools/liftlib","last_synced_at":"2025-03-16T12:17:33.325Z","repository":{"id":168421478,"uuid":"621555012","full_name":"sshtools/liftlib","owner":"sshtools","description":"Library to create an elevated Java process for performing usually restricted actions","archived":false,"fork":false,"pushed_at":"2025-02-27T22:28:56.000Z","size":108,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-02-28T06:12:30.966Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sshtools.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-30T22:46:27.000Z","updated_at":"2025-02-27T22:29:00.000Z","dependencies_parsed_at":null,"dependency_job_id":"1d25c78a-8b02-49bc-b580-d896618be685","html_url":"https://github.com/sshtools/liftlib","commit_stats":null,"previous_names":["sshtools/liftlib"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sshtools%2Fliftlib","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sshtools%2Fliftlib/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sshtools%2Fliftlib/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sshtools%2Fliftlib/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sshtools","download_url":"https://codeload.github.com/sshtools/liftlib/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243864825,"owners_count":20360360,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-22T12:12:02.457Z","updated_at":"2025-03-16T12:17:33.306Z","avatar_url":"https://github.com/sshtools.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LiftLib\n\n![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.sshtools/liftlib/badge.svg)\n[![javadoc](https://javadoc.io/badge2/com.sshtools/liftlib/javadoc.svg)](https://javadoc.io/doc/com.sshtools/liftlib)\n![JPMS](https://img.shields.io/badge/JPMS-com.sshtools.liftlib-purple)\n\nA small, no dependency Java library whose only job is to create an *Elevated JVM* for running small blocks of Java code as a system administrator. The user will be prompted to accept the elevation.\n\nThe code must be wrapped in an `ElevatedClosure`, which implements the `Serializable` interface. There are 2 specializations of this interface for simpler cases, `Run` and `Call`.\n\n## Quick Start\n\n```java\npublic class Test {\n\tpublic static void main(String[] args) throws Exception {\n\t\tvar other = System.getProperty(\"user.name\");\n\t\t\n\t\tSystem.out.println(Elevator.elevator().call(() -\u003e {\n\t\t\treturn \"Hello World to \" + other + \" from \" + System.getProperty(\"user.name\");\n\t\t}));\n\t}\n}\n```\n\nIf run on Linux as the user `joeb`, would result in ..\n\n```\nHello World to joeb from root\n```\n\n## Features\n\n * Creates an optionally re-usable elevated helper to run blocks of code as an administrator or other elevated user.\n * Works with GUI or Console applications.\n * Works with Graal Native Image (in fact works best).\n * Works with Java 17 or above.\n * No JNI, JNA or other FFI, just uses already available operating system commands.\n * Re-authorization timeout for re-usable helpers. After a certain amount of time, elevated actions must be re-authorized.\n * Elevated tasks block while they are running, and accept an input object and return an output object. While running, the elevated code may send back events to the non-elevated code (the reverse is not currently possible).\n\n## Support\n\n*LiftLib* currently support 3 operating systems. \n\n * Linux. Requires that `pkexec` is available.\n * Windows. Requires that `powershell.exe` is available.\n * Mac OS. Requires that `osascript` is available.\n\n## Limitations\n\n * All objects passed to and returned from an elevated helper must be fully `Serializable`. \n * When running in interpreted mode (i.e. not Graal native image), the operating system's elevation prompt will identify the process as *Java*.\n * Only one elevated block of code may be run at a time in any JVM (this may be fixed in a future release).\n * Non-elevated code may not communicate with the elevated code after it has been constructed (again, may be fixed).\n * In interpreted mode, if your original `CLASSPATH` was massive, then so will the elevated helpers' `CLASSPATH`. In practice this shouldn't matter to much, the amount of memory used in the helper will depend on what code is run there. \n\n\n## How Does It Work\n\n*LiftLib* works by reconstructing the command line that was used to launch the application, subtly altering it so that a different `main(String[] args)` method is run, and then executing this using the operating systems native commands \nfor running elevated processes. It then uses either TCP or a Unix Domain Socket to send serializable messages between the two application instances.\n\nThere is a little more to it, to take into account various oddities on the different OSs, but that's the basics.\n  \n 1. Application requests an elevated closure.\n 1. LiftLib sets up a the communications channel server, which will either be a random TCP socket or a Unix Domain Socket, depending on the JDK version.\n 1. LiftLib checks if there is an already running helper. \n 1. If there isn't a helper, or the one that exists has expired, a new helper will be launched. The new helper will be told how to access the communication channel.\n 1. The helper makes a connection back to the communications server. The server will allow no further connections after this.\n 1. LibLib serializes the closure and sends it over the wire.\n 1. The helper de-serializes the closure, runs the code and then serializes a response.\n 1. LifeLib de-serializes the response and returns control to the caller.  \n\n## Usage\n\nThe general pattern is ..\n\n * Obtain an `Elevator` instance, either the default elevator from `Elevator.elevator()`, or configure create a new one with `Elevator.ElevatorBuilder`.\n * Call one of `run()`, `call()` or `closure()` to run your elevated code, passing in an instance of the appropriate interface.\n * `close()` the `Elevator` when you have finished with it (probably do not want to do this if using the default elevator).\n\nThe object instance you pass to one of those methods must be fully `Serializable`, so unless it is run in a `static` context as above, \nit is best to create a formal class rather than using lamba syntax (which may easily include the\nclass it is called from). \n \n### Elevator.run(Run run)\n\nUse this method when you do not require any kind of response (other than exceptions).  \n\n```java\npublic class Test {\n\tpublic static void main(String[] args) throws Exception {\n\t\tnew Test().doCommand();\n\t}\n\t\n\tpublic void doCommand() throws Exception {\n\t\ttry (var elev = new Elevator.ElevatorBuilder().build()) {\n\t\t\telev.run(new Shutdown());\n\t\t}\n\t}\n\n\t@SuppressWarnings(\"serial\")\n\tpublic final static class Shutdown implements Run {\n\t\t\n\t\t@Override\n\t\tpublic void run() throws Exception {\n\t\t\tnew ProcessBuilder(\"shutdown\").start().waitFor();\n\t\t}\n\t}\n}\n```\n \n### Elevator.call(Call\u003cRET\u003e run)\n\nUse this method when you want some kind of response from the elevated code. The response itself\nmust be `Serializable`.\n\n```java\n\n/**\n * A stupid example that doesn't really need to be run as elevated\n */\npublic class Test {\n\tpublic static void main(String[] args) throws Exception {\n\t\tnew Test().doCommand();\n\t}\n\t\n\tpublic void doCommand() throws Exception {\n\t\ttry (var elev = new Elevator.ElevatorBuilder().build()) {\n\t\t\tSystem.out.println(elev.call(new AddTwoNumbers(123,456)));\n\t\t}\n\t}\n\n\tpublic final static class AddTwoNumbers implements Call\u003cInteger\u003e {\n\t\t\n\t\tprivate final int a;\n\t\tprivate final int b;\n\t\t\n\t\tpublic AddTwoNumbers() {\n\t\t\t// Need a default constructor, called in elevated JVM\n\t\t}\n\t\t\n\t\tpublic AddTwoNumbers(int a, int b) {\n\t\t\tthis.a = a;\n\t\t\tthis.b = b;\n\t\t}\n\n\t\t@Override\n\t\tpublic Integer call() throws Exception {\n\t\t\treturn a + b;\n\t\t}\n\t}\n}\n```\n \n### Elevator.closure(ElevatedClosure\u003cRET, EVT\u003e run)\n\nThis method is for more complex needs. It allows you to not only to response with a return value, but also\nfor the elevated code to invoke callbacks in the non-elevated JVM. A `Serializable` parameter may be passed\nto this callback.\n\nThis interface has two generic type parameters, `RET` being the type of the return value, and `EVT` being the\ntype of the event parameter.\n\n```java\n/**\n * A stupid example that tries to kill 10000 processes, starting at PID 10000 \n */\npublic class Test {\n\tpublic static void main(String[] args) throws Exception {\n\t\tnew Test().doCommand();\n\t}\n\t\n\tpublic void doCommand() throws Exception {\n\t\ttry (var elev = new Elevator.ElevatorBuilder().build()) {\n \t\t\tvar count = elev.closure(new KillRange(10000, 20000));\n \t\t\tSystem.out.println(\"I tried to kill \" + count  + \" processes\");\n\t\t}\n\t}\n\n\t@SuppressWarnings(\"serial\")\n\tpublic final static class KillRange implements ElevatedClosure\u003cInteger, Long\u003e {\n\t\t\n\t\tprivate long start;\n\t\tprivate long end;\n\n\t\tpublic KillRange() {\n\t\t\t// Need a default constructor, called in elevated JVM\n\t\t}\n\t\t\n\t\tpublic KillRange(long start, long end) {\n\t\t\tthis.start = start;\n\t\t\tthis.end = end;\n\t\t}\n\n\t\t@Override\n\t\tpublic void event(Long pid) {\n\t\t\t// This is invoked on the non-elevated JVM when the elevated JVM calls `proxy.event(Long)`.\n\t\t\tSystem.out.println(\"The elevated helper just killed process \" + pid);\n\t\t}\n\n\t\t@Override\n\t\tpublic Integer call(ElevatedClosure\u003cInteger, Long\u003e proxy) throws Exception {\n\t\t\tvar killed = 0;\n\t\t\tfor(var i = start ; i \u003c end ; i++) {\n\t\t\t\tnew ProcessBuilder(\"kill\", String.valueOf(i)).start().waitFor();\n\t\t\t\tkilled ++;\n\t\t\t}\n\t\t\treturn killed;\n\t\t}\n\t\t\n\t}\n}\n\n```\n \n## Using With Graal Native Image\n\nTo be compatible with Graal Native Image, you must extend you applications entry point, i.e. your\n`main(String[] args)`. If this methods receives are single element array containing a single argument\nin the format `--elevate=\u003curi\u003e`, where URI will either be an integer number or a file path. For example, \n\n```java\npublic static void main(String[] args) {\n    if(args.length == 1 \u0026\u0026 args[0].startsWith(\"--elevate=\")) {\n        com.sshtools.liftlib.Helper.main(new String[] { args[0].substring(10) });\n    }\n    else {    \n        // Do your normal command line processing / bootstrapping \n    } \n}\n```\n\nYou will also need to ensure that any `ElevatedClosure` implementations you have are added to Graal Native Images configuration as serializable classes.\nThis can be done a number of ways. For example, include a resource at the path `META-INF/native-image/your-app/serialization-config.json` with the content ...\n\n```json\n[\n   {\n    \"name\":\"com.acme.MyElevatedThing\"\n  }\n]\n```\n  \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsshtools%2Fliftlib","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsshtools%2Fliftlib","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsshtools%2Fliftlib/lists"}