rda-python-setuid 1.0.9__tar.gz → 3.0.1__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 (26) hide show
  1. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/MANIFEST.in +1 -0
  2. rda_python_setuid-3.0.1/PKG-INFO +275 -0
  3. rda_python_setuid-3.0.1/README.md +259 -0
  4. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/pyproject.toml +2 -2
  5. rda_python_setuid-3.0.1/src/rda_python_setuid/cmwrapper.c +85 -0
  6. rda_python_setuid-3.0.1/src/rda_python_setuid/install.py +330 -0
  7. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid/install.usg +77 -17
  8. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid/pgstart.py +3 -3
  9. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid/pywrapper.py +2 -0
  10. rda_python_setuid-3.0.1/src/rda_python_setuid/setuid_setup.usg +75 -0
  11. rda_python_setuid-3.0.1/src/rda_python_setuid/setup_guide.py +51 -0
  12. rda_python_setuid-3.0.1/src/rda_python_setuid.egg-info/PKG-INFO +275 -0
  13. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/SOURCES.txt +3 -0
  14. rda_python_setuid-1.0.9/PKG-INFO +0 -147
  15. rda_python_setuid-1.0.9/README.md +0 -131
  16. rda_python_setuid-1.0.9/src/rda_python_setuid/install.py +0 -161
  17. rda_python_setuid-1.0.9/src/rda_python_setuid.egg-info/PKG-INFO +0 -147
  18. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/LICENSE +0 -0
  19. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/setup.cfg +0 -0
  20. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid/__init__.py +0 -0
  21. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid/pywrapper.c +0 -0
  22. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/dependency_links.txt +0 -0
  23. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/entry_points.txt +0 -0
  24. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/requires.txt +0 -0
  25. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/top_level.txt +0 -0
  26. {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/tests/test_setuid.py +0 -0
@@ -1,2 +1,3 @@
1
1
  include src/rda_python_setuid/pywrapper.c
2
+ include src/rda_python_setuid/cmwrapper.c
2
3
  include src/rda_python_setuid/install.usg
@@ -0,0 +1,275 @@
1
+ Metadata-Version: 2.4
2
+ Name: rda_python_setuid
3
+ Version: 3.0.1
4
+ Summary: RDA Python Package to setuid for program executions as an effective or common user
5
+ Author-email: Zaihua Ji <zji@ucar.edu>
6
+ Project-URL: Homepage, https://github.com/NCAR/rda-python-setuid
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: rda_python_common
15
+ Dynamic: license-file
16
+
17
+ RDA Python package, including a C code wrapper, to execute commandline applications
18
+ via setuid for effective and common user names.
19
+
20
+ ## Overview
21
+
22
+ `rda_python_setuid` provides a C binary (`pywrapper`) that acquires a setuid effective
23
+ user, then `execv`s a Python entry point script. This allows Python programs to run
24
+ as a designated common user (e.g. `gdexdata`) without requiring `sudo` access.
25
+
26
+ Three modes are supported:
27
+
28
+ - **Mode 1 (CommonUser program):** a symlink `dsarch -> pywrapper` runs `setuid_dsarch`
29
+ as the common user.
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).
34
+ - **Mode 3 (cmwrapper, callers outside the group):** a dedicated binary compiled
35
+ from `cmwrapper.c` and installed `4755` runs ONE fixed program as the common
36
+ user for any user on the machine, including users outside the common user's
37
+ group (see "cmwrapper" below).
38
+
39
+ Two Python entry points are packaged alongside the C wrapper:
40
+
41
+ - **`pywrapper.py`** — the default fallback target executed when `pywrapper.c`
42
+ cannot resolve a matching `setuid_<program>` entry point. Acquires the
43
+ effective UID via `PgLOG.set_suid()`, prints the caller's real and effective
44
+ user names, and shows the `pyproject.toml` snippet plus the
45
+ `pywrapper-install -l <program>` command needed to wrap a new script.
46
+ Diagnostic flags `-env`, `-inc`, and `-plg` dump the environment variables,
47
+ `sys.path`, and `PGLOG` dictionary respectively — handy for verifying the
48
+ setuid environment before wiring up a real program.
49
+
50
+ - **`pgstart.py`** — the Mode 2 launcher invoked through a `pgstart_<loginname>`
51
+ copy of `pywrapper`. Reads the real/effective UIDs from `PGLOG`, then
52
+ permits execution only if the real user is in
53
+ `[PGLOG['ADMINUSER'], euser, PGLOG['COMMONUSER']]`
54
+ (i.e. the admin specialist `PGLOG['ADMINUSER']` — default `zji` — the
55
+ effective user themselves, or the shared common user); unauthorized callers receive
56
+ an informational message and exit. After authorization it parses leading
57
+ flag tokens — `-bg` (background via `subprocess.Popen`), `-fg` (explicit
58
+ foreground, default), `-cwd <dir>` (chdir before exec), and the same
59
+ `-env`/`-inc`/`-plg` diagnostics as `pywrapper.py` — and then runs the
60
+ remaining arguments as a command (`subprocess.run`/`Popen`) under the
61
+ effective UID, logging a host/program/timestamp/user line to `pgstart.log`.
62
+
63
+ ## Dependency requirement
64
+
65
+ Any Python package whose programs are to be run via the setuid mechanism must declare
66
+ `rda_python_setuid` as a dependency in its `pyproject.toml`:
67
+
68
+ ```toml
69
+ [project]
70
+ dependencies = [
71
+ "rda_python_setuid",
72
+ ...
73
+ ]
74
+ ```
75
+
76
+ It must also register each wrapped program's connector entry point with a `setuid_`
77
+ prefix:
78
+
79
+ ```toml
80
+ [project.scripts]
81
+ "setuid_dsarch" = "rda_python_dsarch.dsarch:main"
82
+ ```
83
+
84
+ `pip install` then places `setuid_dsarch` in the environment's `bin/` directory
85
+ automatically. `pywrapper-install -l/--link` creates the symlink
86
+ `dsarch -> pywrapper`; running `dsarch` goes through the setuid wrapper, which
87
+ execs `setuid_dsarch` as CommonUser.
88
+
89
+ The `main()` of each wrapped program (e.g. `rda_python_dsarch/dsarch.py`) must
90
+ also call `show_setup_guide()` at the top of `main()`, passing an instance of
91
+ the program's class along with the package name and list of setuid program
92
+ names:
93
+
94
+ ```python
95
+ def main():
96
+ from rda_python_setuid.setup_guide import show_setup_guide
97
+ object = DsArch()
98
+ show_setup_guide(object, 'rda_python_dsarch', ['dsarch'])
99
+ ...
100
+ ```
101
+
102
+ When `setuid_dsarch` is invoked directly (before pywrapper symlinks are set
103
+ up, so euid ≠ CommonUser), `show_setup_guide()` prints the shared setuid setup
104
+ guide and exits. When invoked via the `dsarch -> pywrapper` symlink (euid =
105
+ CommonUser), `get_command()` strips the `setuid_` prefix, the check inside
106
+ `show_setup_guide()` fails, and the program runs normally.
107
+
108
+ ## Environment setup
109
+
110
+ Create a Python environment first; package installs in the next section run
111
+ inside whichever environment you activate here.
112
+
113
+ ### Option A — Python venv (DECS machines)
114
+
115
+ ```bash
116
+ python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
117
+ source $ENVHOME/bin/activate
118
+ ```
119
+
120
+ ### Option B — Conda (DAV/Casper)
121
+
122
+ ```bash
123
+ conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
124
+ conda activate $ENVHOME
125
+ ```
126
+
127
+ ## Installing rda-python-setuid
128
+
129
+ Pick whichever install mode fits your workflow. All four pull in the
130
+ transitive dependency (`rda_python_common`) automatically. Once installed,
131
+ the `pywrapper-install` CLI is available for the setuid wiring steps below.
132
+
133
+ For local development, clone this repo alongside your project and install it
134
+ in editable mode so that changes are picked up without re-installing:
135
+
136
+ ```bash
137
+ git clone https://github.com/NCAR/rda-python-setuid.git
138
+ cd rda-python-setuid
139
+ pip install -e .
140
+ ```
141
+
142
+ To test a specific branch (e.g. an in-progress feature or fix branch), pass
143
+ `-b/--branch` to `git clone`:
144
+
145
+ ```bash
146
+ git clone -b <branch-name> https://github.com/NCAR/rda-python-setuid.git
147
+ cd rda-python-setuid
148
+ pip install -e .
149
+ ```
150
+
151
+ For a regular (non-editable) install from a checkout:
152
+
153
+ ```bash
154
+ pip install /path/to/rda-python-setuid
155
+ ```
156
+
157
+ For a production install on a system that uses the published distribution:
158
+
159
+ ```bash
160
+ pip install rda_python_setuid
161
+ ```
162
+
163
+ ## Setuid wrapper setup
164
+
165
+ With `rda_python_setuid` installed in the active environment, run
166
+ `pywrapper-install` with no arguments to display the full user guide:
167
+
168
+ ```bash
169
+ pywrapper-install
170
+ ```
171
+
172
+ ### Full setuid setup (requires sudo access to CommonUser)
173
+
174
+ ```bash
175
+ # 1. Install the target package (pulls in rda_python_setuid automatically):
176
+ pip install rda_python_dsarch
177
+
178
+ # 2. Compile pywrapper C binary (once per environment):
179
+ pywrapper-install -c|--compile
180
+
181
+ # 3. Wire up each program as a setuid entry (specify name or use 'all'):
182
+ pywrapper-install -l|--link dsarch
183
+ pywrapper-install -l|--link all # auto-link every setuid_* entry not yet linked
184
+
185
+ # 4. Optionally, install a pgstart_<loginname> binary so <loginname> (any user
186
+ # in the same group as PGLOG['COMMONUSER']) can run commands as themselves
187
+ # via the setuid wrapper. Same command in both cases — only the invoker
188
+ # differs:
189
+ #
190
+ # 4a. If PGLOG['ADMINUSER'] (default zji) can `sudo -u <loginname>`, the
191
+ # admin sets it up on the user's behalf:
192
+ pywrapper-install -p|--pgstart -n|--username <loginname>
193
+ #
194
+ # 4b. Otherwise <loginname> runs the same command themselves (no sudo
195
+ # from ADMINUSER required, since they already are <loginname>):
196
+ pywrapper-install -p|--pgstart -n|--username <loginname>
197
+ ```
198
+
199
+ ### Update an existing installation (no sudo required)
200
+
201
+ When the package is upgraded and a new `pywrapper.c` is bundled, use `-u/--update`
202
+ to recompile and reinstall all setuid binaries without needing `sudo`. The existing
203
+ `pgstart_*` binaries in `bin/` are used to perform the privileged operations:
204
+
205
+ ```bash
206
+ pywrapper-install -u|--update [-n|--username gdexdata] [-e|--envhome $ENVHOME]
207
+ ```
208
+
209
+ ### Simple install (no sudo required, runs as current user)
210
+
211
+ Users who do not need the setuid mechanism can skip steps 2–4 and create a
212
+ direct symlink from `dsarch` to `setuid_dsarch`:
213
+
214
+ ```bash
215
+ pip install rda_python_dsarch
216
+ pywrapper-install -l|--link dsarch -s|--simple
217
+ pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
218
+ ```
219
+
220
+ ### cmwrapper (Mode 3, for callers outside the CommonUser group)
221
+
222
+ `pywrapper` is installed `4750`, so only members of the common user's group can
223
+ execute it. That group restriction is what makes it safe for `pywrapper` to pick
224
+ the program to run from `basename(argv[0])`: anyone who can execute it can already
225
+ reach every `setuid_*` entry in `bin/` just by naming the symlink differently.
226
+
227
+ `cmwrapper` is for the opposite case — letting users who are **not** in the common
228
+ user's group run one specific program as the common user. It is installed `4755`,
229
+ i.e. executable by everyone, so nothing about what it runs may come from the caller:
230
+
231
+ - the absolute path of the script to exec, and the program name, are baked into the
232
+ binary at compile time, so it cannot be symlinked under another name to reach a
233
+ different program;
234
+ - the environment is replaced with a fixed whitelist (`HOME`, `USER`, `LOGNAME`,
235
+ `TERM`, `LANG`, `TZ`, a fixed `PATH` and `PYTHONNOUSERSITE=1`), so `PYTHONPATH`,
236
+ `PYTHONHOME`, `LD_PRELOAD`, `LD_LIBRARY_PATH` and the `PGLOG` path variables
237
+ (`DSDHOME`, `DSSHOME`, `LOGPATH`, `COMMONUSER`, ...) cannot be used to run
238
+ arbitrary code, or redirect where files are written, as the common user.
239
+
240
+ One binary is compiled per wrapped program:
241
+
242
+ ```bash
243
+ # Install bin/gdexdrop as a 4755 binary that execs bin/setuid_gdexdrop:
244
+ pywrapper-install -m|--cmlink gdexdrop
245
+
246
+ # Same, but installed into a common area already on everyone's PATH:
247
+ pywrapper-install -m|--cmlink gdexdrop -d|--destdir /glade/u/home/gdexdata/bin
248
+
249
+ # Point it at a script somewhere other than bin/setuid_gdexdrop:
250
+ pywrapper-install -m|--cmlink gdexdrop -t|--target /path/to/setuid_gdexdrop
251
+ ```
252
+
253
+ Only wrap a program that does its own authorization and confines where it writes,
254
+ such as `gdexdrop`, which checks its caller against an access list and copies only
255
+ into the requested dataset directory. Never wrap a general purpose program such as
256
+ `gdexcp`: at `4755` that would let any user on the machine read or overwrite any
257
+ file of the common user.
258
+
259
+ ## Runtime flow
260
+
261
+ ```
262
+ user runs: dsarch [args]
263
+ | (symlink -> pywrapper, setuid bit -> EUID=gdexdata)
264
+ pywrapper.c: execv(bin/setuid_dsarch, args)
265
+ setuid_dsarch: calls dsarch:main() as gdexdata
266
+
267
+ user runs: gdexdrop [args]
268
+ | (dedicated 4755 binary, setuid bit -> EUID=gdexdata)
269
+ cmwrapper.c: execve(bin/setuid_gdexdrop, args, sanitized env)
270
+ setuid_gdexdrop: calls gdexdrop:main() as gdexdata
271
+ ```
272
+
273
+ ## Github
274
+
275
+ <https://github.com/NCAR/rda-python-setuid>
@@ -0,0 +1,259 @@
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
+ Three 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
+ - **Mode 3 (cmwrapper, callers outside the group):** a dedicated binary compiled
19
+ from `cmwrapper.c` and installed `4755` runs ONE fixed program as the common
20
+ user for any user on the machine, including users outside the common user's
21
+ group (see "cmwrapper" below).
22
+
23
+ Two Python entry points are packaged alongside the C wrapper:
24
+
25
+ - **`pywrapper.py`** — the default fallback target executed when `pywrapper.c`
26
+ cannot resolve a matching `setuid_<program>` entry point. Acquires the
27
+ effective UID via `PgLOG.set_suid()`, prints the caller's real and effective
28
+ user names, and shows the `pyproject.toml` snippet plus the
29
+ `pywrapper-install -l <program>` command needed to wrap a new script.
30
+ Diagnostic flags `-env`, `-inc`, and `-plg` dump the environment variables,
31
+ `sys.path`, and `PGLOG` dictionary respectively — handy for verifying the
32
+ setuid environment before wiring up a real program.
33
+
34
+ - **`pgstart.py`** — the Mode 2 launcher invoked through a `pgstart_<loginname>`
35
+ copy of `pywrapper`. Reads the real/effective UIDs from `PGLOG`, then
36
+ permits execution only if the real user is in
37
+ `[PGLOG['ADMINUSER'], euser, PGLOG['COMMONUSER']]`
38
+ (i.e. the admin specialist `PGLOG['ADMINUSER']` — default `zji` — the
39
+ effective user themselves, or the shared common user); unauthorized callers receive
40
+ an informational message and exit. After authorization it parses leading
41
+ flag tokens — `-bg` (background via `subprocess.Popen`), `-fg` (explicit
42
+ foreground, default), `-cwd <dir>` (chdir before exec), and the same
43
+ `-env`/`-inc`/`-plg` diagnostics as `pywrapper.py` — and then runs the
44
+ remaining arguments as a command (`subprocess.run`/`Popen`) under the
45
+ effective UID, logging a host/program/timestamp/user line to `pgstart.log`.
46
+
47
+ ## Dependency requirement
48
+
49
+ Any Python package whose programs are to be run via the setuid mechanism must declare
50
+ `rda_python_setuid` as a dependency in its `pyproject.toml`:
51
+
52
+ ```toml
53
+ [project]
54
+ dependencies = [
55
+ "rda_python_setuid",
56
+ ...
57
+ ]
58
+ ```
59
+
60
+ It must also register each wrapped program's connector entry point with a `setuid_`
61
+ prefix:
62
+
63
+ ```toml
64
+ [project.scripts]
65
+ "setuid_dsarch" = "rda_python_dsarch.dsarch:main"
66
+ ```
67
+
68
+ `pip install` then places `setuid_dsarch` in the environment's `bin/` directory
69
+ automatically. `pywrapper-install -l/--link` creates the symlink
70
+ `dsarch -> pywrapper`; running `dsarch` goes through the setuid wrapper, which
71
+ execs `setuid_dsarch` as CommonUser.
72
+
73
+ The `main()` of each wrapped program (e.g. `rda_python_dsarch/dsarch.py`) must
74
+ also call `show_setup_guide()` at the top of `main()`, passing an instance of
75
+ the program's class along with the package name and list of setuid program
76
+ names:
77
+
78
+ ```python
79
+ def main():
80
+ from rda_python_setuid.setup_guide import show_setup_guide
81
+ object = DsArch()
82
+ show_setup_guide(object, 'rda_python_dsarch', ['dsarch'])
83
+ ...
84
+ ```
85
+
86
+ When `setuid_dsarch` is invoked directly (before pywrapper symlinks are set
87
+ up, so euid ≠ CommonUser), `show_setup_guide()` prints the shared setuid setup
88
+ guide and exits. When invoked via the `dsarch -> pywrapper` symlink (euid =
89
+ CommonUser), `get_command()` strips the `setuid_` prefix, the check inside
90
+ `show_setup_guide()` fails, and the program runs normally.
91
+
92
+ ## Environment setup
93
+
94
+ Create a Python environment first; package installs in the next section run
95
+ inside whichever environment you activate here.
96
+
97
+ ### Option A — Python venv (DECS machines)
98
+
99
+ ```bash
100
+ python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
101
+ source $ENVHOME/bin/activate
102
+ ```
103
+
104
+ ### Option B — Conda (DAV/Casper)
105
+
106
+ ```bash
107
+ conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
108
+ conda activate $ENVHOME
109
+ ```
110
+
111
+ ## Installing rda-python-setuid
112
+
113
+ Pick whichever install mode fits your workflow. All four pull in the
114
+ transitive dependency (`rda_python_common`) automatically. Once installed,
115
+ the `pywrapper-install` CLI is available for the setuid wiring steps below.
116
+
117
+ For local development, clone this repo alongside your project and install it
118
+ in editable mode so that changes are picked up without re-installing:
119
+
120
+ ```bash
121
+ git clone https://github.com/NCAR/rda-python-setuid.git
122
+ cd rda-python-setuid
123
+ pip install -e .
124
+ ```
125
+
126
+ To test a specific branch (e.g. an in-progress feature or fix branch), pass
127
+ `-b/--branch` to `git clone`:
128
+
129
+ ```bash
130
+ git clone -b <branch-name> https://github.com/NCAR/rda-python-setuid.git
131
+ cd rda-python-setuid
132
+ pip install -e .
133
+ ```
134
+
135
+ For a regular (non-editable) install from a checkout:
136
+
137
+ ```bash
138
+ pip install /path/to/rda-python-setuid
139
+ ```
140
+
141
+ For a production install on a system that uses the published distribution:
142
+
143
+ ```bash
144
+ pip install rda_python_setuid
145
+ ```
146
+
147
+ ## Setuid wrapper setup
148
+
149
+ With `rda_python_setuid` installed in the active environment, run
150
+ `pywrapper-install` with no arguments to display the full user guide:
151
+
152
+ ```bash
153
+ pywrapper-install
154
+ ```
155
+
156
+ ### Full setuid setup (requires sudo access to CommonUser)
157
+
158
+ ```bash
159
+ # 1. Install the target package (pulls in rda_python_setuid automatically):
160
+ pip install rda_python_dsarch
161
+
162
+ # 2. Compile pywrapper C binary (once per environment):
163
+ pywrapper-install -c|--compile
164
+
165
+ # 3. Wire up each program as a setuid entry (specify name or use 'all'):
166
+ pywrapper-install -l|--link dsarch
167
+ pywrapper-install -l|--link all # auto-link every setuid_* entry not yet linked
168
+
169
+ # 4. Optionally, install a pgstart_<loginname> binary so <loginname> (any user
170
+ # in the same group as PGLOG['COMMONUSER']) can run commands as themselves
171
+ # via the setuid wrapper. Same command in both cases — only the invoker
172
+ # differs:
173
+ #
174
+ # 4a. If PGLOG['ADMINUSER'] (default zji) can `sudo -u <loginname>`, the
175
+ # admin sets it up on the user's behalf:
176
+ pywrapper-install -p|--pgstart -n|--username <loginname>
177
+ #
178
+ # 4b. Otherwise <loginname> runs the same command themselves (no sudo
179
+ # from ADMINUSER required, since they already are <loginname>):
180
+ pywrapper-install -p|--pgstart -n|--username <loginname>
181
+ ```
182
+
183
+ ### Update an existing installation (no sudo required)
184
+
185
+ When the package is upgraded and a new `pywrapper.c` is bundled, use `-u/--update`
186
+ to recompile and reinstall all setuid binaries without needing `sudo`. The existing
187
+ `pgstart_*` binaries in `bin/` are used to perform the privileged operations:
188
+
189
+ ```bash
190
+ pywrapper-install -u|--update [-n|--username gdexdata] [-e|--envhome $ENVHOME]
191
+ ```
192
+
193
+ ### Simple install (no sudo required, runs as current user)
194
+
195
+ Users who do not need the setuid mechanism can skip steps 2–4 and create a
196
+ direct symlink from `dsarch` to `setuid_dsarch`:
197
+
198
+ ```bash
199
+ pip install rda_python_dsarch
200
+ pywrapper-install -l|--link dsarch -s|--simple
201
+ pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
202
+ ```
203
+
204
+ ### cmwrapper (Mode 3, for callers outside the CommonUser group)
205
+
206
+ `pywrapper` is installed `4750`, so only members of the common user's group can
207
+ execute it. That group restriction is what makes it safe for `pywrapper` to pick
208
+ the program to run from `basename(argv[0])`: anyone who can execute it can already
209
+ reach every `setuid_*` entry in `bin/` just by naming the symlink differently.
210
+
211
+ `cmwrapper` is for the opposite case — letting users who are **not** in the common
212
+ user's group run one specific program as the common user. It is installed `4755`,
213
+ i.e. executable by everyone, so nothing about what it runs may come from the caller:
214
+
215
+ - the absolute path of the script to exec, and the program name, are baked into the
216
+ binary at compile time, so it cannot be symlinked under another name to reach a
217
+ different program;
218
+ - the environment is replaced with a fixed whitelist (`HOME`, `USER`, `LOGNAME`,
219
+ `TERM`, `LANG`, `TZ`, a fixed `PATH` and `PYTHONNOUSERSITE=1`), so `PYTHONPATH`,
220
+ `PYTHONHOME`, `LD_PRELOAD`, `LD_LIBRARY_PATH` and the `PGLOG` path variables
221
+ (`DSDHOME`, `DSSHOME`, `LOGPATH`, `COMMONUSER`, ...) cannot be used to run
222
+ arbitrary code, or redirect where files are written, as the common user.
223
+
224
+ One binary is compiled per wrapped program:
225
+
226
+ ```bash
227
+ # Install bin/gdexdrop as a 4755 binary that execs bin/setuid_gdexdrop:
228
+ pywrapper-install -m|--cmlink gdexdrop
229
+
230
+ # Same, but installed into a common area already on everyone's PATH:
231
+ pywrapper-install -m|--cmlink gdexdrop -d|--destdir /glade/u/home/gdexdata/bin
232
+
233
+ # Point it at a script somewhere other than bin/setuid_gdexdrop:
234
+ pywrapper-install -m|--cmlink gdexdrop -t|--target /path/to/setuid_gdexdrop
235
+ ```
236
+
237
+ Only wrap a program that does its own authorization and confines where it writes,
238
+ such as `gdexdrop`, which checks its caller against an access list and copies only
239
+ into the requested dataset directory. Never wrap a general purpose program such as
240
+ `gdexcp`: at `4755` that would let any user on the machine read or overwrite any
241
+ file of the common user.
242
+
243
+ ## Runtime flow
244
+
245
+ ```
246
+ user runs: dsarch [args]
247
+ | (symlink -> pywrapper, setuid bit -> EUID=gdexdata)
248
+ pywrapper.c: execv(bin/setuid_dsarch, args)
249
+ setuid_dsarch: calls dsarch:main() as gdexdata
250
+
251
+ user runs: gdexdrop [args]
252
+ | (dedicated 4755 binary, setuid bit -> EUID=gdexdata)
253
+ cmwrapper.c: execve(bin/setuid_gdexdrop, args, sanitized env)
254
+ setuid_gdexdrop: calls gdexdrop:main() as gdexdata
255
+ ```
256
+
257
+ ## Github
258
+
259
+ <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.1"
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", "cmwrapper.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,85 @@
1
+ /***************************************************************************************\
2
+ *
3
+ * Title: cmwrapper.c
4
+ * Author: Zaihua Ji, zji@ucar.edu
5
+ * Date: 2026-09-23
6
+ * Purpose: C wrapper to start ONE fixed python program as an effective user, for
7
+ * callers who are NOT in the common user's group (Mode 4755).
8
+ *
9
+ * Difference from pywrapper.c:
10
+ * pywrapper is installed 4750, so only members of the common user's group can
11
+ * execute it, and it picks the program to exec from basename(argv[0]) with the
12
+ * directory taken from /proc/self/exe. That is safe only because of the 4750
13
+ * group restriction: anyone who can run it can already run every setuid_* entry
14
+ * in the bin directory just by naming the symlink differently.
15
+ *
16
+ * cmwrapper is installed 4755, i.e. executable by EVERY user on the machine, so
17
+ * neither the program nor its directory may come from the caller:
18
+ * - the absolute path of the program to exec is baked in at compile time as
19
+ * CMEXEC, and the program name reported to the program is baked in as CMPROG;
20
+ * - the environment is replaced with a fixed whitelist, so PYTHONPATH,
21
+ * PYTHONHOME, LD_PRELOAD, LD_LIBRARY_PATH and the PGLOG path variables
22
+ * (DSDHOME, DSSHOME, LOGPATH, COMMONUSER, ...) cannot be used to run
23
+ * arbitrary code, or redirect where files are written, as the common user.
24
+ *
25
+ * One cmwrapper binary is therefore compiled per wrapped program; it can NOT be
26
+ * symlinked under another name to reach a different program.
27
+ *
28
+ * IMPORTANT: a program wrapped by cmwrapper is callable by anybody, so it must do
29
+ * its own authorization (as gdexdrop does with its access list) and must confine
30
+ * where it writes. Do NOT wrap a general purpose program such as gdexcp.
31
+ *
32
+ * Instruction:
33
+ * # Compile and install bin/PROGRAM as a 4755 setuid binary owned by CommonUser:
34
+ * pywrapper-install -m|--cmlink PROGRAM [-n|--username CommonUser] [-e|--envhome $ENVHOME]
35
+ *
36
+ \***************************************************************************************/
37
+
38
+ #include <unistd.h>
39
+ #include <stdio.h>
40
+ #include <string.h>
41
+ #include <stdlib.h>
42
+
43
+ #ifndef CMPROG
44
+ #error "CMPROG must be defined at compile time, e.g. -DCMPROG=\"gdexdrop\""
45
+ #endif
46
+ #ifndef CMEXEC
47
+ #error "CMEXEC must be defined at compile time, e.g. -DCMEXEC=\"/env/bin/setuid_gdexdrop\""
48
+ #endif
49
+
50
+ #define CMPATH "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
51
+
52
+ /* Environment variables passed through from the caller. Everything else is
53
+ dropped; in particular every PYTHON*, LD_* and PGLOG path variable. */
54
+ static const char *keepvars[] = {"HOME", "USER", "LOGNAME", "TERM", "LANG", "TZ", NULL};
55
+
56
+ /* main program */
57
+ int main(int argc, char *argv[]) {
58
+ (void)argc;
59
+
60
+ char *envp[16];
61
+ char *value, *entry;
62
+ size_t len;
63
+ int i, n = 0;
64
+
65
+ for(i = 0; keepvars[i] != NULL; i++) {
66
+ value = getenv(keepvars[i]);
67
+ if(value == NULL) continue;
68
+ len = strlen(keepvars[i]) + strlen(value) + 2;
69
+ entry = malloc(len);
70
+ if(entry == NULL) {
71
+ perror(CMPROG ": malloc");
72
+ exit(1);
73
+ }
74
+ snprintf(entry, len, "%s=%s", keepvars[i], value);
75
+ envp[n++] = entry;
76
+ }
77
+ envp[n++] = (char *)"PATH=" CMPATH;
78
+ envp[n++] = (char *)"PYTHONNOUSERSITE=1"; /* ignore ~/.local site-packages */
79
+ envp[n] = NULL;
80
+
81
+ argv[0] = (char *)CMPROG; /* the program identifies itself by this name */
82
+ execve(CMEXEC, argv, envp);
83
+ perror(CMEXEC); /* execve only returns on error */
84
+ exit(1);
85
+ }