rda-python-setuid 1.0.9__tar.gz → 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {rda_python_setuid-1.0.9/src/rda_python_setuid.egg-info → rda_python_setuid-3.0.0}/PKG-INFO +97 -17
  2. rda_python_setuid-3.0.0/README.md +211 -0
  3. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/pyproject.toml +2 -2
  4. rda_python_setuid-3.0.0/src/rda_python_setuid/install.py +286 -0
  5. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid/install.usg +47 -17
  6. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid/pgstart.py +3 -3
  7. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid/pywrapper.py +2 -0
  8. rda_python_setuid-3.0.0/src/rda_python_setuid/setuid_setup.usg +75 -0
  9. rda_python_setuid-3.0.0/src/rda_python_setuid/setup_guide.py +51 -0
  10. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0/src/rda_python_setuid.egg-info}/PKG-INFO +97 -17
  11. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid.egg-info/SOURCES.txt +2 -0
  12. rda_python_setuid-1.0.9/README.md +0 -131
  13. rda_python_setuid-1.0.9/src/rda_python_setuid/install.py +0 -161
  14. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/LICENSE +0 -0
  15. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/MANIFEST.in +0 -0
  16. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/setup.cfg +0 -0
  17. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid/__init__.py +0 -0
  18. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid/pywrapper.c +0 -0
  19. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid.egg-info/dependency_links.txt +0 -0
  20. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid.egg-info/entry_points.txt +0 -0
  21. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid.egg-info/requires.txt +0 -0
  22. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid.egg-info/top_level.txt +0 -0
  23. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/tests/test_setuid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_setuid
3
- Version: 1.0.9
3
+ Version: 3.0.0
4
4
  Summary: RDA Python Package to setuid for program executions as an effective or common user
5
5
  Author-email: Zaihua Ji <zji@ucar.edu>
6
6
  Project-URL: Homepage, https://github.com/NCAR/rda-python-setuid
@@ -27,8 +27,10 @@ Two modes are supported:
27
27
 
28
28
  - **Mode 1 (CommonUser program):** a symlink `dsarch -> pywrapper` runs `setuid_dsarch`
29
29
  as the common user.
30
- - **Mode 2 (pgstart specialist):** a copy `pgstart_zji` runs any command as specialist
31
- `zji` via `pgstart.py`, restricted to authorized users.
30
+ - **Mode 2 (pgstart specialist):** a copy `pgstart_<loginname>` (e.g. `pgstart_zji`)
31
+ runs any command as `<loginname>` via `pgstart.py`. `<loginname>` can be any
32
+ user that belongs to the same group as `PGLOG['COMMONUSER']`. Execution is
33
+ restricted to authorized callers (see `pgstart.py` below).
32
34
 
33
35
  Two Python entry points are packaged alongside the C wrapper:
34
36
 
@@ -41,10 +43,12 @@ Two Python entry points are packaged alongside the C wrapper:
41
43
  `sys.path`, and `PGLOG` dictionary respectively — handy for verifying the
42
44
  setuid environment before wiring up a real program.
43
45
 
44
- - **`pgstart.py`** — the Mode 2 launcher invoked through a `pgstart_<USER>`
46
+ - **`pgstart.py`** — the Mode 2 launcher invoked through a `pgstart_<loginname>`
45
47
  copy of `pywrapper`. Reads the real/effective UIDs from `PGLOG`, then
46
- permits execution only if the real user matches the effective user or the
47
- shared GDEX common user (`PGLOG['GDEXUSER']`); unauthorized callers receive
48
+ permits execution only if the real user is in
49
+ `[PGLOG['ADMINUSER'], euser, PGLOG['COMMONUSER']]`
50
+ (i.e. the admin specialist `PGLOG['ADMINUSER']` — default `zji` — the
51
+ effective user themselves, or the shared common user); unauthorized callers receive
48
52
  an informational message and exit. After authorization it parses leading
49
53
  flag tokens — `-bg` (background via `subprocess.Popen`), `-fg` (explicit
50
54
  foreground, default), `-cwd <dir>` (chdir before exec), and the same
@@ -78,30 +82,84 @@ automatically. `pywrapper-install -l/--link` creates the symlink
78
82
  `dsarch -> pywrapper`; running `dsarch` goes through the setuid wrapper, which
79
83
  execs `setuid_dsarch` as CommonUser.
80
84
 
85
+ The `main()` of each wrapped program (e.g. `rda_python_dsarch/dsarch.py`) must
86
+ also call `show_setup_guide()` at the top of `main()`, passing an instance of
87
+ the program's class along with the package name and list of setuid program
88
+ names:
89
+
90
+ ```python
91
+ def main():
92
+ from rda_python_setuid.setup_guide import show_setup_guide
93
+ object = DsArch()
94
+ show_setup_guide(object, 'rda_python_dsarch', ['dsarch'])
95
+ ...
96
+ ```
97
+
98
+ When `setuid_dsarch` is invoked directly (before pywrapper symlinks are set
99
+ up, so euid ≠ CommonUser), `show_setup_guide()` prints the shared setuid setup
100
+ guide and exits. When invoked via the `dsarch -> pywrapper` symlink (euid =
101
+ CommonUser), `get_command()` strips the `setuid_` prefix, the check inside
102
+ `show_setup_guide()` fails, and the program runs normally.
103
+
81
104
  ## Environment setup
82
105
 
106
+ Create a Python environment first; package installs in the next section run
107
+ inside whichever environment you activate here.
108
+
83
109
  ### Option A — Python venv (DECS machines)
84
110
 
85
111
  ```bash
86
112
  python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
87
113
  source $ENVHOME/bin/activate
88
- pip install rda_python_setuid rda_python_dsarch ...
89
114
  ```
90
115
 
91
116
  ### Option B — Conda (DAV/Casper)
92
117
 
93
118
  ```bash
94
- conda create -n pg-gdex python=3.10
95
- conda activate pg-gdex
96
- pip install rda_python_setuid rda_python_dsarch ...
119
+ conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
120
+ conda activate $ENVHOME
97
121
  ```
98
122
 
99
- The conda environment is typically at `/glade/work/gdexdata/conda-envs/pg-gdex`.
123
+ ## Installing rda-python-setuid
100
124
 
101
- ## Installation
125
+ Pick whichever install mode fits your workflow. All four pull in the
126
+ transitive dependency (`rda_python_common`) automatically. Once installed,
127
+ the `pywrapper-install` CLI is available for the setuid wiring steps below.
102
128
 
103
- After setting up the environment and installing packages, run `pywrapper-install`
104
- with no arguments to display the full user guide:
129
+ For local development, clone this repo alongside your project and install it
130
+ in editable mode so that changes are picked up without re-installing:
131
+
132
+ ```bash
133
+ git clone https://github.com/NCAR/rda-python-setuid.git
134
+ cd rda-python-setuid
135
+ pip install -e .
136
+ ```
137
+
138
+ To test a specific branch (e.g. an in-progress feature or fix branch), pass
139
+ `-b/--branch` to `git clone`:
140
+
141
+ ```bash
142
+ git clone -b <branch-name> https://github.com/NCAR/rda-python-setuid.git
143
+ cd rda-python-setuid
144
+ pip install -e .
145
+ ```
146
+
147
+ For a regular (non-editable) install from a checkout:
148
+
149
+ ```bash
150
+ pip install /path/to/rda-python-setuid
151
+ ```
152
+
153
+ For a production install on a system that uses the published distribution:
154
+
155
+ ```bash
156
+ pip install rda_python_setuid
157
+ ```
158
+
159
+ ## Setuid wrapper setup
160
+
161
+ With `rda_python_setuid` installed in the active environment, run
162
+ `pywrapper-install` with no arguments to display the full user guide:
105
163
 
106
164
  ```bash
107
165
  pywrapper-install
@@ -116,11 +174,32 @@ pip install rda_python_dsarch
116
174
  # 2. Compile pywrapper C binary (once per environment):
117
175
  pywrapper-install -c|--compile
118
176
 
119
- # 3. Wire up each program as a setuid entry:
177
+ # 3. Wire up each program as a setuid entry (specify name or use 'all'):
120
178
  pywrapper-install -l|--link dsarch
179
+ pywrapper-install -l|--link all # auto-link every setuid_* entry not yet linked
180
+
181
+ # 4. Optionally, install a pgstart_<loginname> binary so <loginname> (any user
182
+ # in the same group as PGLOG['COMMONUSER']) can run commands as themselves
183
+ # via the setuid wrapper. Same command in both cases — only the invoker
184
+ # differs:
185
+ #
186
+ # 4a. If PGLOG['ADMINUSER'] (default zji) can `sudo -u <loginname>`, the
187
+ # admin sets it up on the user's behalf:
188
+ pywrapper-install -p|--pgstart -n|--username <loginname>
189
+ #
190
+ # 4b. Otherwise <loginname> runs the same command themselves (no sudo
191
+ # from ADMINUSER required, since they already are <loginname>):
192
+ pywrapper-install -p|--pgstart -n|--username <loginname>
193
+ ```
121
194
 
122
- # 4. Optionally, allow a specialist to run commands as themselves:
123
- pywrapper-install -p|--pgstart -u|--user zji
195
+ ### Update an existing installation (no sudo required)
196
+
197
+ When the package is upgraded and a new `pywrapper.c` is bundled, use `-u/--update`
198
+ to recompile and reinstall all setuid binaries without needing `sudo`. The existing
199
+ `pgstart_*` binaries in `bin/` are used to perform the privileged operations:
200
+
201
+ ```bash
202
+ pywrapper-install -u|--update [-n|--username gdexdata] [-e|--envhome $ENVHOME]
124
203
  ```
125
204
 
126
205
  ### Simple install (no sudo required, runs as current user)
@@ -131,6 +210,7 @@ direct symlink from `dsarch` to `setuid_dsarch`:
131
210
  ```bash
132
211
  pip install rda_python_dsarch
133
212
  pywrapper-install -l|--link dsarch -s|--simple
213
+ pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
134
214
  ```
135
215
 
136
216
  ## Runtime flow
@@ -0,0 +1,211 @@
1
+ RDA Python package, including a C code wrapper, to execute commandline applications
2
+ via setuid for effective and common user names.
3
+
4
+ ## Overview
5
+
6
+ `rda_python_setuid` provides a C binary (`pywrapper`) that acquires a setuid effective
7
+ user, then `execv`s a Python entry point script. This allows Python programs to run
8
+ as a designated common user (e.g. `gdexdata`) without requiring `sudo` access.
9
+
10
+ Two modes are supported:
11
+
12
+ - **Mode 1 (CommonUser program):** a symlink `dsarch -> pywrapper` runs `setuid_dsarch`
13
+ as the common user.
14
+ - **Mode 2 (pgstart specialist):** a copy `pgstart_<loginname>` (e.g. `pgstart_zji`)
15
+ runs any command as `<loginname>` via `pgstart.py`. `<loginname>` can be any
16
+ user that belongs to the same group as `PGLOG['COMMONUSER']`. Execution is
17
+ restricted to authorized callers (see `pgstart.py` below).
18
+
19
+ Two Python entry points are packaged alongside the C wrapper:
20
+
21
+ - **`pywrapper.py`** — the default fallback target executed when `pywrapper.c`
22
+ cannot resolve a matching `setuid_<program>` entry point. Acquires the
23
+ effective UID via `PgLOG.set_suid()`, prints the caller's real and effective
24
+ user names, and shows the `pyproject.toml` snippet plus the
25
+ `pywrapper-install -l <program>` command needed to wrap a new script.
26
+ Diagnostic flags `-env`, `-inc`, and `-plg` dump the environment variables,
27
+ `sys.path`, and `PGLOG` dictionary respectively — handy for verifying the
28
+ setuid environment before wiring up a real program.
29
+
30
+ - **`pgstart.py`** — the Mode 2 launcher invoked through a `pgstart_<loginname>`
31
+ copy of `pywrapper`. Reads the real/effective UIDs from `PGLOG`, then
32
+ permits execution only if the real user is in
33
+ `[PGLOG['ADMINUSER'], euser, PGLOG['COMMONUSER']]`
34
+ (i.e. the admin specialist `PGLOG['ADMINUSER']` — default `zji` — the
35
+ effective user themselves, or the shared common user); unauthorized callers receive
36
+ an informational message and exit. After authorization it parses leading
37
+ flag tokens — `-bg` (background via `subprocess.Popen`), `-fg` (explicit
38
+ foreground, default), `-cwd <dir>` (chdir before exec), and the same
39
+ `-env`/`-inc`/`-plg` diagnostics as `pywrapper.py` — and then runs the
40
+ remaining arguments as a command (`subprocess.run`/`Popen`) under the
41
+ effective UID, logging a host/program/timestamp/user line to `pgstart.log`.
42
+
43
+ ## Dependency requirement
44
+
45
+ Any Python package whose programs are to be run via the setuid mechanism must declare
46
+ `rda_python_setuid` as a dependency in its `pyproject.toml`:
47
+
48
+ ```toml
49
+ [project]
50
+ dependencies = [
51
+ "rda_python_setuid",
52
+ ...
53
+ ]
54
+ ```
55
+
56
+ It must also register each wrapped program's connector entry point with a `setuid_`
57
+ prefix:
58
+
59
+ ```toml
60
+ [project.scripts]
61
+ "setuid_dsarch" = "rda_python_dsarch.dsarch:main"
62
+ ```
63
+
64
+ `pip install` then places `setuid_dsarch` in the environment's `bin/` directory
65
+ automatically. `pywrapper-install -l/--link` creates the symlink
66
+ `dsarch -> pywrapper`; running `dsarch` goes through the setuid wrapper, which
67
+ execs `setuid_dsarch` as CommonUser.
68
+
69
+ The `main()` of each wrapped program (e.g. `rda_python_dsarch/dsarch.py`) must
70
+ also call `show_setup_guide()` at the top of `main()`, passing an instance of
71
+ the program's class along with the package name and list of setuid program
72
+ names:
73
+
74
+ ```python
75
+ def main():
76
+ from rda_python_setuid.setup_guide import show_setup_guide
77
+ object = DsArch()
78
+ show_setup_guide(object, 'rda_python_dsarch', ['dsarch'])
79
+ ...
80
+ ```
81
+
82
+ When `setuid_dsarch` is invoked directly (before pywrapper symlinks are set
83
+ up, so euid ≠ CommonUser), `show_setup_guide()` prints the shared setuid setup
84
+ guide and exits. When invoked via the `dsarch -> pywrapper` symlink (euid =
85
+ CommonUser), `get_command()` strips the `setuid_` prefix, the check inside
86
+ `show_setup_guide()` fails, and the program runs normally.
87
+
88
+ ## Environment setup
89
+
90
+ Create a Python environment first; package installs in the next section run
91
+ inside whichever environment you activate here.
92
+
93
+ ### Option A — Python venv (DECS machines)
94
+
95
+ ```bash
96
+ python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
97
+ source $ENVHOME/bin/activate
98
+ ```
99
+
100
+ ### Option B — Conda (DAV/Casper)
101
+
102
+ ```bash
103
+ conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
104
+ conda activate $ENVHOME
105
+ ```
106
+
107
+ ## Installing rda-python-setuid
108
+
109
+ Pick whichever install mode fits your workflow. All four pull in the
110
+ transitive dependency (`rda_python_common`) automatically. Once installed,
111
+ the `pywrapper-install` CLI is available for the setuid wiring steps below.
112
+
113
+ For local development, clone this repo alongside your project and install it
114
+ in editable mode so that changes are picked up without re-installing:
115
+
116
+ ```bash
117
+ git clone https://github.com/NCAR/rda-python-setuid.git
118
+ cd rda-python-setuid
119
+ pip install -e .
120
+ ```
121
+
122
+ To test a specific branch (e.g. an in-progress feature or fix branch), pass
123
+ `-b/--branch` to `git clone`:
124
+
125
+ ```bash
126
+ git clone -b <branch-name> https://github.com/NCAR/rda-python-setuid.git
127
+ cd rda-python-setuid
128
+ pip install -e .
129
+ ```
130
+
131
+ For a regular (non-editable) install from a checkout:
132
+
133
+ ```bash
134
+ pip install /path/to/rda-python-setuid
135
+ ```
136
+
137
+ For a production install on a system that uses the published distribution:
138
+
139
+ ```bash
140
+ pip install rda_python_setuid
141
+ ```
142
+
143
+ ## Setuid wrapper setup
144
+
145
+ With `rda_python_setuid` installed in the active environment, run
146
+ `pywrapper-install` with no arguments to display the full user guide:
147
+
148
+ ```bash
149
+ pywrapper-install
150
+ ```
151
+
152
+ ### Full setuid setup (requires sudo access to CommonUser)
153
+
154
+ ```bash
155
+ # 1. Install the target package (pulls in rda_python_setuid automatically):
156
+ pip install rda_python_dsarch
157
+
158
+ # 2. Compile pywrapper C binary (once per environment):
159
+ pywrapper-install -c|--compile
160
+
161
+ # 3. Wire up each program as a setuid entry (specify name or use 'all'):
162
+ pywrapper-install -l|--link dsarch
163
+ pywrapper-install -l|--link all # auto-link every setuid_* entry not yet linked
164
+
165
+ # 4. Optionally, install a pgstart_<loginname> binary so <loginname> (any user
166
+ # in the same group as PGLOG['COMMONUSER']) can run commands as themselves
167
+ # via the setuid wrapper. Same command in both cases — only the invoker
168
+ # differs:
169
+ #
170
+ # 4a. If PGLOG['ADMINUSER'] (default zji) can `sudo -u <loginname>`, the
171
+ # admin sets it up on the user's behalf:
172
+ pywrapper-install -p|--pgstart -n|--username <loginname>
173
+ #
174
+ # 4b. Otherwise <loginname> runs the same command themselves (no sudo
175
+ # from ADMINUSER required, since they already are <loginname>):
176
+ pywrapper-install -p|--pgstart -n|--username <loginname>
177
+ ```
178
+
179
+ ### Update an existing installation (no sudo required)
180
+
181
+ When the package is upgraded and a new `pywrapper.c` is bundled, use `-u/--update`
182
+ to recompile and reinstall all setuid binaries without needing `sudo`. The existing
183
+ `pgstart_*` binaries in `bin/` are used to perform the privileged operations:
184
+
185
+ ```bash
186
+ pywrapper-install -u|--update [-n|--username gdexdata] [-e|--envhome $ENVHOME]
187
+ ```
188
+
189
+ ### Simple install (no sudo required, runs as current user)
190
+
191
+ Users who do not need the setuid mechanism can skip steps 2–4 and create a
192
+ direct symlink from `dsarch` to `setuid_dsarch`:
193
+
194
+ ```bash
195
+ pip install rda_python_dsarch
196
+ pywrapper-install -l|--link dsarch -s|--simple
197
+ pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
198
+ ```
199
+
200
+ ## Runtime flow
201
+
202
+ ```
203
+ user runs: dsarch [args]
204
+ | (symlink -> pywrapper, setuid bit -> EUID=gdexdata)
205
+ pywrapper.c: execv(bin/setuid_dsarch, args)
206
+ setuid_dsarch: calls dsarch:main() as gdexdata
207
+ ```
208
+
209
+ ## Github
210
+
211
+ <https://github.com/NCAR/rda-python-setuid>
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "rda_python_setuid"
9
- version = "1.0.9"
9
+ version = "3.0.0"
10
10
  authors = [
11
11
  { name="Zaihua Ji", email="zji@ucar.edu" },
12
12
  ]
@@ -30,7 +30,7 @@ include-package-data = true
30
30
  where = ["src"]
31
31
 
32
32
  [tool.setuptools.package-data]
33
- "rda_python_setuid" = ["pywrapper.c", "install.usg"]
33
+ "rda_python_setuid" = ["pywrapper.c", "install.usg", "setuid_setup.usg"]
34
34
 
35
35
  [project.urls]
36
36
  "Homepage" = "https://github.com/NCAR/rda-python-setuid"
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ ##################################################################################
4
+ #
5
+ # Title: pywrapper-install
6
+ # Author: Zaihua Ji, zji@ucar.edu
7
+ # Date: 2025-05-11
8
+ # Purpose: Install helper for the pywrapper setuid C binary.
9
+ # Replaces the manual gcc/chmod/ln steps with a single command.
10
+ #
11
+ # Github: https://github.com/NCAR/rda-python-setuid.git
12
+ #
13
+ # Usage:
14
+ # # 0. Display this user guide:
15
+ # pywrapper-install
16
+ #
17
+ # # 1. Compile and install pywrapper (run once per environment):
18
+ # pywrapper-install -c [-n gdexdata] [-e $ENVHOME]
19
+ #
20
+ # # 2. Create pgstart_USER entry so USER can run commands as themselves:
21
+ # pywrapper-install -p [-n zji] [-e $ENVHOME]
22
+ #
23
+ # # 3. Create a symlink so a program runs as CommonUser via pywrapper (setuid):
24
+ # pywrapper-install -l myprog [-n gdexdata] [-e $ENVHOME]
25
+ #
26
+ # # 3b. Auto-link all discovered setuid_* entries that are not yet linked:
27
+ # pywrapper-install -l all [-e $ENVHOME]
28
+ #
29
+ # # 4. Simple install: symlink PROGRAM -> setuid_PROGRAM (no setuid, runs as current user):
30
+ # pywrapper-install -l myprog -s [-e $ENVHOME]
31
+ #
32
+ # # 5. Update existing installation (recompile and reinstall all setuid binaries):
33
+ # pywrapper-install -u [-n gdexdata] [-e $ENVHOME]
34
+ #
35
+ # Convention for wrapped programs:
36
+ # The target package must register its connector entry point with a setuid_ prefix:
37
+ # [project.scripts]
38
+ # "setuid_dsarch" = "rda_python_dsarch.dsarch:main"
39
+ # pip install places setuid_dsarch in the bin dir automatically.
40
+ # pywrapper-install --link dsarch creates the symlink dsarch -> pywrapper, so
41
+ # users invoking dsarch go through the setuid wrapper which execs setuid_dsarch
42
+ # as CommonUser.
43
+ # pywrapper-install --link dsarch --simple creates dsarch -> setuid_dsarch directly,
44
+ # skipping setuid; the program runs as the current user.
45
+ #
46
+ ##################################################################################
47
+
48
+ import argparse
49
+ import os
50
+ import shutil
51
+ import subprocess
52
+ import sys
53
+
54
+
55
+ def get_bindir():
56
+ """Return the bin directory of the active Python environment."""
57
+ return os.path.dirname(os.path.abspath(sys.executable))
58
+
59
+
60
+ def get_c_source():
61
+ """Return path to pywrapper.c bundled with this package."""
62
+ return os.path.join(os.path.dirname(__file__), 'pywrapper.c')
63
+
64
+
65
+ def run(cmd):
66
+ print(" $", " ".join(cmd))
67
+ subprocess.run(cmd, check=True)
68
+
69
+
70
+ def show_usage():
71
+ usgfile = os.path.join(os.path.dirname(__file__), 'install.usg')
72
+ os.system("more " + usgfile)
73
+ sys.exit(0)
74
+
75
+
76
+ def main():
77
+
78
+ parser = argparse.ArgumentParser(
79
+ description="Compile and install the pywrapper setuid C binary."
80
+ )
81
+ parser.add_argument(
82
+ '-e', '--envhome', default=None,
83
+ help="Path to the venv root directory containing bin/ (default: parent of the current Python executable's bin/ dir)"
84
+ )
85
+ parser.add_argument(
86
+ '-n', '--username', default=None,
87
+ help="User name to own the setuid binary (default: current login user for -p/--pgstart, gdexdata otherwise)"
88
+ )
89
+ parser.add_argument(
90
+ '-s', '--simple', action='store_true',
91
+ help="Simple install: create symlink PROGRAM -> setuid_PROGRAM, skipping setuid (use with -l/--link)"
92
+ )
93
+ group = parser.add_mutually_exclusive_group()
94
+ group.add_argument(
95
+ '-c', '--compile', action='store_true',
96
+ help="Compile pywrapper.c and install bin/pywrapper as a setuid binary (run once per environment)"
97
+ )
98
+ group.add_argument(
99
+ '-p', '--pgstart', action='store_true',
100
+ help="Create pgstart_USER for running commands as USER (Mode 2)"
101
+ )
102
+ group.add_argument(
103
+ '-l', '--link', metavar='PROGRAM',
104
+ help="Create symlink PROGRAM -> pywrapper for running a fixed program as CommonUser (Mode 1); use 'all' to auto-link every setuid_* entry not yet linked"
105
+ )
106
+ group.add_argument(
107
+ '-u', '--update', action='store_true',
108
+ help="Update an existing installation: recompile pywrapper and reinstall all pgstart_USER setuid binaries"
109
+ )
110
+ args = parser.parse_args()
111
+
112
+ if not (args.compile or args.pgstart or args.link or args.update):
113
+ show_usage()
114
+
115
+ if args.username is None and not args.simple:
116
+ import pwd
117
+ if args.pgstart:
118
+ args.username = pwd.getpwuid(os.getuid()).pw_name
119
+ else:
120
+ args.username = 'gdexdata'
121
+
122
+ bindir = os.path.join(args.envhome, 'bin') if args.envhome else get_bindir()
123
+ pywrapper = os.path.join(bindir, 'pywrapper')
124
+
125
+ if args.link:
126
+ # For appname -> pywrapper links, run `ln -s` via pgstart_<commonuser> so
127
+ # the resulting symlink is owned by the common user (pywrapper owner).
128
+ pgstart_common = None
129
+ if not args.simple and os.path.exists(pywrapper):
130
+ import pwd
131
+ common_user = pwd.getpwuid(os.stat(pywrapper).st_uid).pw_name
132
+ pgstart_common = os.path.join(bindir, 'pgstart_' + common_user)
133
+ if not os.path.exists(pgstart_common):
134
+ print("Error: {} not found. Run pywrapper-install --pgstart --username {} first.".format(pgstart_common, common_user))
135
+ sys.exit(1)
136
+
137
+ if args.link.lower() == 'all':
138
+ # Discover all setuid_* entries in bindir and link any that are missing
139
+ appnames = sorted(
140
+ f[len('setuid_'):] for f in os.listdir(bindir) if f.startswith('setuid_')
141
+ )
142
+ if not appnames:
143
+ print("No setuid_* entries found in {}".format(bindir))
144
+ for appname in appnames:
145
+ target = os.path.join(bindir, appname)
146
+ script = os.path.join(bindir, 'setuid_' + appname)
147
+ if args.simple:
148
+ if os.path.lexists(target):
149
+ print("Already exists: {}".format(target))
150
+ else:
151
+ os.symlink(script, target)
152
+ print("Created: {} -> setuid_{}".format(target, appname))
153
+ else:
154
+ if os.path.lexists(target):
155
+ print("Already exists: {}".format(target))
156
+ else:
157
+ run([pgstart_common, 'ln', '-s', pywrapper, target])
158
+ print("Created: {} -> pywrapper".format(target))
159
+ else:
160
+ target = os.path.join(bindir, args.link)
161
+ script = os.path.join(bindir, 'setuid_{}'.format(args.link))
162
+ if not os.path.exists(script):
163
+ print("Error: {} not found. Install the package that provides it first.".format(script))
164
+ sys.exit(1)
165
+ if args.simple:
166
+ # Simple install: symlink PROGRAM -> setuid_PROGRAM, no setuid, runs as current user.
167
+ if os.path.lexists(target):
168
+ print("Already exists: {}".format(target))
169
+ else:
170
+ os.symlink(script, target)
171
+ print("Created: {} -> setuid_{}".format(target, args.link))
172
+ else:
173
+ # Mode 1: symlink PROGRAM -> pywrapper. setuid_PROGRAM is left with its
174
+ # default ownership/permissions so it can be loaded and executed normally.
175
+ if os.path.lexists(target):
176
+ print("Already exists: {}".format(target))
177
+ else:
178
+ run([pgstart_common, 'ln', '-s', pywrapper, target])
179
+ print("Created: {} -> pywrapper".format(target))
180
+
181
+ elif args.pgstart:
182
+ # Mode 2: create pgstart_USER with setuid owned by USER.
183
+ # When USER already owns pywrapper (i.e. the common user), pywrapper is
184
+ # already setuid owned by USER, so a symlink is sufficient; otherwise
185
+ # copy pywrapper and chmod 4750 as USER.
186
+ if not os.path.exists(pywrapper):
187
+ print("Error: {} not found. Run pywrapper-install --compile --username COMMONUSER first.".format(pywrapper))
188
+ sys.exit(1)
189
+ target = os.path.join(bindir, 'pgstart_{}'.format(args.username))
190
+ import pwd
191
+ pywrapper_owner = pwd.getpwuid(os.stat(pywrapper).st_uid).pw_name
192
+ if args.username == pywrapper_owner:
193
+ if os.path.lexists(target):
194
+ os.remove(target)
195
+ os.symlink(pywrapper, target)
196
+ print("Linked: {} -> pywrapper (setuid, owned by {})".format(target, args.username))
197
+ else:
198
+ curuser = pwd.getpwuid(os.getuid()).pw_name
199
+ sudo_prefix = [] if curuser == args.username else ['sudo', '-u', args.username]
200
+ run(sudo_prefix + ['cp', pywrapper, target])
201
+ run(sudo_prefix + ['chmod', '4750', target])
202
+ print("Installed: {} (setuid, owned by {})".format(target, args.username))
203
+
204
+ elif args.compile:
205
+ # Compile pywrapper.c and install pywrapper with setuid
206
+ src = get_c_source()
207
+ src_dest = os.path.join(bindir, 'pywrapper.c')
208
+ shutil.copy(src, src_dest)
209
+ print("Copied: {}".format(src_dest))
210
+ run(['sudo', '-u', args.username, 'gcc', '-o', pywrapper, src_dest])
211
+ run(['sudo', '-u', args.username, 'chmod', '4750', pywrapper])
212
+ print("Installed: {} (setuid, owned by {})".format(pywrapper, args.username))
213
+
214
+ elif args.update:
215
+ # Update an existing installation: recompile pywrapper and reinstall all setuid binaries
216
+ pgstart_files = sorted(f for f in os.listdir(bindir) if f.startswith('pgstart_'))
217
+ if not pgstart_files:
218
+ print("Error: No pgstart_* binaries found in {}".format(bindir))
219
+ sys.exit(1)
220
+ if not os.path.exists(pywrapper):
221
+ print("Error: {} not found.".format(pywrapper))
222
+ sys.exit(1)
223
+
224
+ gdexuser = args.username
225
+ pgstart_gdexdata = os.path.join(bindir, 'pgstart_' + gdexuser)
226
+ if not os.path.exists(pgstart_gdexdata):
227
+ print("Error: {} not found.".format(pgstart_gdexdata))
228
+ sys.exit(1)
229
+
230
+ update_tmp = os.path.join(bindir, 'update_tmp')
231
+ os.makedirs(update_tmp, exist_ok=True)
232
+ print("Created: {}".format(update_tmp))
233
+
234
+ # Symlink pgstart.py into update_tmp so pywrapper instances running from
235
+ # update_tmp can find it via the fpath/pgstart.py fallback lookup.
236
+ bindir_pgstart_py = os.path.join(bindir, 'pgstart.py')
237
+ update_pgstart_py = os.path.join(update_tmp, 'pgstart.py')
238
+ if not os.path.lexists(update_pgstart_py):
239
+ os.symlink(bindir_pgstart_py, update_pgstart_py)
240
+ print("Linked: {} -> {}".format(update_pgstart_py, bindir_pgstart_py))
241
+
242
+ # Copy each non-gdex pgstart_USERNAME into update_tmp using itself, then chmod 4750
243
+ for fname in pgstart_files:
244
+ username = fname[len('pgstart_'):]
245
+ if username == gdexuser:
246
+ continue
247
+ src_pgstart = os.path.join(bindir, fname)
248
+ dst_pgstart = os.path.join(update_tmp, fname)
249
+ run([src_pgstart, 'cp', src_pgstart, update_tmp + '/'])
250
+ run([src_pgstart, 'chmod', '4750', dst_pgstart])
251
+
252
+ # Copy pywrapper into update_tmp as gdexdata, chmod, then hardlink as pgstart_gdexdata
253
+ update_pywrapper = os.path.join(update_tmp, 'pywrapper')
254
+ update_pgstart_gdexdata = os.path.join(update_tmp, 'pgstart_' + gdexuser)
255
+ run([pgstart_gdexdata, 'cp', pywrapper, update_tmp + '/'])
256
+ run([pgstart_gdexdata, 'chmod', '4750', update_pywrapper])
257
+ run([pgstart_gdexdata, 'ln', update_pywrapper, update_pgstart_gdexdata])
258
+
259
+ # Compile new pywrapper using update_tmp/pgstart_gdexdata
260
+ src = get_c_source()
261
+ src_dest = os.path.join(bindir, 'pywrapper.c')
262
+ shutil.copy(src, src_dest)
263
+ print("Copied: {}".format(src_dest))
264
+ run([update_pgstart_gdexdata, 'gcc', '-o', pywrapper, src_dest])
265
+ run([update_pgstart_gdexdata, 'chmod', '4750', pywrapper])
266
+ print("Compiled: {} (setuid, owned by {})".format(pywrapper, gdexuser))
267
+
268
+ # Recreate each pgstart_* in bindir using the corresponding update_tmp/pgstart_*
269
+ for fname in sorted(f for f in os.listdir(update_tmp) if f.startswith('pgstart_')):
270
+ username = fname[len('pgstart_'):]
271
+ update_pgstart = os.path.join(update_tmp, fname)
272
+ target = os.path.join(bindir, fname)
273
+ if username == gdexuser:
274
+ run([update_pgstart_gdexdata, 'ln', '-sf', pywrapper, target])
275
+ print("Linked: {} -> pywrapper (setuid, owned by {})".format(target, gdexuser))
276
+ else:
277
+ run([update_pgstart, 'cp', pywrapper, target])
278
+ run([update_pgstart, 'chmod', '4750', target])
279
+ print("Updated: {} (setuid, owned by {})".format(target, username))
280
+
281
+ # Clean up the temporary working directory
282
+ shutil.rmtree(update_tmp)
283
+ print("Removed: {}".format(update_tmp))
284
+
285
+
286
+ if __name__ == '__main__': main()