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.
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/MANIFEST.in +1 -0
- rda_python_setuid-3.0.1/PKG-INFO +275 -0
- rda_python_setuid-3.0.1/README.md +259 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/pyproject.toml +2 -2
- rda_python_setuid-3.0.1/src/rda_python_setuid/cmwrapper.c +85 -0
- rda_python_setuid-3.0.1/src/rda_python_setuid/install.py +330 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid/install.usg +77 -17
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid/pgstart.py +3 -3
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid/pywrapper.py +2 -0
- rda_python_setuid-3.0.1/src/rda_python_setuid/setuid_setup.usg +75 -0
- rda_python_setuid-3.0.1/src/rda_python_setuid/setup_guide.py +51 -0
- rda_python_setuid-3.0.1/src/rda_python_setuid.egg-info/PKG-INFO +275 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/SOURCES.txt +3 -0
- rda_python_setuid-1.0.9/PKG-INFO +0 -147
- rda_python_setuid-1.0.9/README.md +0 -131
- rda_python_setuid-1.0.9/src/rda_python_setuid/install.py +0 -161
- rda_python_setuid-1.0.9/src/rda_python_setuid.egg-info/PKG-INFO +0 -147
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/LICENSE +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/setup.cfg +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid/__init__.py +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid/pywrapper.c +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/dependency_links.txt +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/entry_points.txt +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/requires.txt +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/top_level.txt +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.1}/tests/test_setuid.py +0 -0
|
@@ -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 = "
|
|
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
|
+
}
|