rda-python-setuid 3.0.0__tar.gz → 3.0.2__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 (22) hide show
  1. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/MANIFEST.in +1 -0
  2. {rda_python_setuid-3.0.0/src/rda_python_setuid.egg-info → rda_python_setuid-3.0.2}/PKG-INFO +75 -2
  3. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/README.md +74 -1
  4. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/pyproject.toml +2 -2
  5. rda_python_setuid-3.0.2/src/rda_python_setuid/cmwrapper.c +120 -0
  6. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid/install.py +65 -5
  7. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid/install.usg +53 -7
  8. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2/src/rda_python_setuid.egg-info}/PKG-INFO +75 -2
  9. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid.egg-info/SOURCES.txt +1 -0
  10. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/LICENSE +0 -0
  11. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/setup.cfg +0 -0
  12. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid/__init__.py +0 -0
  13. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid/pgstart.py +0 -0
  14. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid/pywrapper.c +0 -0
  15. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid/pywrapper.py +0 -0
  16. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid/setuid_setup.usg +0 -0
  17. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid/setup_guide.py +0 -0
  18. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid.egg-info/dependency_links.txt +0 -0
  19. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid.egg-info/entry_points.txt +0 -0
  20. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid.egg-info/requires.txt +0 -0
  21. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/src/rda_python_setuid.egg-info/top_level.txt +0 -0
  22. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.2}/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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_setuid
3
- Version: 3.0.0
3
+ Version: 3.0.2
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
@@ -23,7 +23,7 @@ via setuid for effective and common user names.
23
23
  user, then `execv`s a Python entry point script. This allows Python programs to run
24
24
  as a designated common user (e.g. `gdexdata`) without requiring `sudo` access.
25
25
 
26
- Two modes are supported:
26
+ Three modes are supported:
27
27
 
28
28
  - **Mode 1 (CommonUser program):** a symlink `dsarch -> pywrapper` runs `setuid_dsarch`
29
29
  as the common user.
@@ -31,6 +31,12 @@ Two modes are supported:
31
31
  runs any command as `<loginname>` via `pgstart.py`. `<loginname>` can be any
32
32
  user that belongs to the same group as `PGLOG['COMMONUSER']`. Execution is
33
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). With `-s|--simple` the same binary is installed
38
+ `755` with no setuid, which publishes ONE program of this environment to users
39
+ who have no environment of their own.
34
40
 
35
41
  Two Python entry points are packaged alongside the C wrapper:
36
42
 
@@ -213,6 +219,63 @@ pywrapper-install -l|--link dsarch -s|--simple
213
219
  pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
214
220
  ```
215
221
 
222
+ ### cmwrapper (Mode 3, for callers outside the CommonUser group)
223
+
224
+ `pywrapper` is installed `4750`, so only members of the common user's group can
225
+ execute it. That group restriction is what makes it safe for `pywrapper` to pick
226
+ the program to run from `basename(argv[0])`: anyone who can execute it can already
227
+ reach every `setuid_*` entry in `bin/` just by naming the symlink differently.
228
+
229
+ `cmwrapper` is for the opposite case — letting users who are **not** in the common
230
+ user's group run one specific program as the common user. It is installed `4755`,
231
+ i.e. executable by everyone, so nothing about what it runs may come from the caller:
232
+
233
+ - the absolute path of the script to exec, and the program name, are baked into the
234
+ binary at compile time, so it cannot be symlinked under another name to reach a
235
+ different program;
236
+ - the environment is replaced with a fixed whitelist (`HOME`, `USER`, `LOGNAME`,
237
+ `TERM`, `LANG`, `TZ`, a fixed `PATH` and `PYTHONNOUSERSITE=1`), so `PYTHONPATH`,
238
+ `PYTHONHOME`, `LD_PRELOAD`, `LD_LIBRARY_PATH` and the `PGLOG` path variables
239
+ (`DSDHOME`, `DSSHOME`, `LOGPATH`, `COMMONUSER`, ...) cannot be used to run
240
+ arbitrary code, or redirect where files are written, as the common user.
241
+
242
+ One binary is compiled per wrapped program:
243
+
244
+ ```bash
245
+ # Install bin/gdexdrop as a 4755 binary that execs bin/setuid_gdexdrop:
246
+ pywrapper-install -m|--cmlink gdexdrop
247
+
248
+ # Same, but installed into a common area already on everyone's PATH:
249
+ pywrapper-install -m|--cmlink gdexdrop -d|--destdir /glade/u/home/gdexdata/bin
250
+
251
+ # Point it at a script somewhere other than bin/setuid_gdexdrop:
252
+ pywrapper-install -m|--cmlink gdexdrop -t|--target /path/to/setuid_gdexdrop
253
+ ```
254
+
255
+ Only wrap a program that does its own authorization and confines where it writes,
256
+ such as `gdexdrop`, which checks its caller against an access list and copies only
257
+ into the requested dataset directory. Never wrap a general purpose program such as
258
+ `gdexcp`: at `4755` that would let any user on the machine read or overwrite any
259
+ file of the common user.
260
+
261
+ ### cmwrapper with no setuid, to publish a program of this environment
262
+
263
+ Add `-s|--simple` to compile the binary `755` with no setuid bit. Nothing runs as
264
+ another user; the binary exists only so that users who do not have this environment,
265
+ and cannot activate a venv or a conda environment, can still run one of its programs
266
+ by name — `gdexls`, for example:
267
+
268
+ ```bash
269
+ # Install /glade/u/apps/contrib/gdexls, a 755 binary that execs bin/gdexls:
270
+ pywrapper-install -m|--cmlink gdexls -s|--simple -d|--destdir /glade/u/apps/contrib
271
+ ```
272
+
273
+ Since there is no privilege change, sanitizing the environment would buy no security
274
+ and would only break the caller's own settings, so the caller's environment is passed
275
+ through except for `PYTHONPATH`, `PYTHONHOME` and `PYTHONSTARTUP`, which would send
276
+ the program to another environment's modules. The default `-t|--target` is
277
+ `bin/PROGRAM` rather than `bin/setuid_PROGRAM`, and no `sudo` access is required.
278
+
216
279
  ## Runtime flow
217
280
 
218
281
  ```
@@ -220,6 +283,16 @@ user runs: dsarch [args]
220
283
  | (symlink -> pywrapper, setuid bit -> EUID=gdexdata)
221
284
  pywrapper.c: execv(bin/setuid_dsarch, args)
222
285
  setuid_dsarch: calls dsarch:main() as gdexdata
286
+
287
+ user runs: gdexdrop [args]
288
+ | (dedicated 4755 binary, setuid bit -> EUID=gdexdata)
289
+ cmwrapper.c: execve(bin/setuid_gdexdrop, args, sanitized env)
290
+ setuid_gdexdrop: calls gdexdrop:main() as gdexdata
291
+
292
+ user runs: gdexls [args]
293
+ | (dedicated 755 binary, no setuid, -DCMSIMPLE)
294
+ cmwrapper.c: execv(bin/gdexls, args)
295
+ gdexls: calls gdexls:main() as the calling user
223
296
  ```
224
297
 
225
298
  ## Github
@@ -7,7 +7,7 @@ via setuid for effective and common user names.
7
7
  user, then `execv`s a Python entry point script. This allows Python programs to run
8
8
  as a designated common user (e.g. `gdexdata`) without requiring `sudo` access.
9
9
 
10
- Two modes are supported:
10
+ Three modes are supported:
11
11
 
12
12
  - **Mode 1 (CommonUser program):** a symlink `dsarch -> pywrapper` runs `setuid_dsarch`
13
13
  as the common user.
@@ -15,6 +15,12 @@ Two modes are supported:
15
15
  runs any command as `<loginname>` via `pgstart.py`. `<loginname>` can be any
16
16
  user that belongs to the same group as `PGLOG['COMMONUSER']`. Execution is
17
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). With `-s|--simple` the same binary is installed
22
+ `755` with no setuid, which publishes ONE program of this environment to users
23
+ who have no environment of their own.
18
24
 
19
25
  Two Python entry points are packaged alongside the C wrapper:
20
26
 
@@ -197,6 +203,63 @@ pywrapper-install -l|--link dsarch -s|--simple
197
203
  pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
198
204
  ```
199
205
 
206
+ ### cmwrapper (Mode 3, for callers outside the CommonUser group)
207
+
208
+ `pywrapper` is installed `4750`, so only members of the common user's group can
209
+ execute it. That group restriction is what makes it safe for `pywrapper` to pick
210
+ the program to run from `basename(argv[0])`: anyone who can execute it can already
211
+ reach every `setuid_*` entry in `bin/` just by naming the symlink differently.
212
+
213
+ `cmwrapper` is for the opposite case — letting users who are **not** in the common
214
+ user's group run one specific program as the common user. It is installed `4755`,
215
+ i.e. executable by everyone, so nothing about what it runs may come from the caller:
216
+
217
+ - the absolute path of the script to exec, and the program name, are baked into the
218
+ binary at compile time, so it cannot be symlinked under another name to reach a
219
+ different program;
220
+ - the environment is replaced with a fixed whitelist (`HOME`, `USER`, `LOGNAME`,
221
+ `TERM`, `LANG`, `TZ`, a fixed `PATH` and `PYTHONNOUSERSITE=1`), so `PYTHONPATH`,
222
+ `PYTHONHOME`, `LD_PRELOAD`, `LD_LIBRARY_PATH` and the `PGLOG` path variables
223
+ (`DSDHOME`, `DSSHOME`, `LOGPATH`, `COMMONUSER`, ...) cannot be used to run
224
+ arbitrary code, or redirect where files are written, as the common user.
225
+
226
+ One binary is compiled per wrapped program:
227
+
228
+ ```bash
229
+ # Install bin/gdexdrop as a 4755 binary that execs bin/setuid_gdexdrop:
230
+ pywrapper-install -m|--cmlink gdexdrop
231
+
232
+ # Same, but installed into a common area already on everyone's PATH:
233
+ pywrapper-install -m|--cmlink gdexdrop -d|--destdir /glade/u/home/gdexdata/bin
234
+
235
+ # Point it at a script somewhere other than bin/setuid_gdexdrop:
236
+ pywrapper-install -m|--cmlink gdexdrop -t|--target /path/to/setuid_gdexdrop
237
+ ```
238
+
239
+ Only wrap a program that does its own authorization and confines where it writes,
240
+ such as `gdexdrop`, which checks its caller against an access list and copies only
241
+ into the requested dataset directory. Never wrap a general purpose program such as
242
+ `gdexcp`: at `4755` that would let any user on the machine read or overwrite any
243
+ file of the common user.
244
+
245
+ ### cmwrapper with no setuid, to publish a program of this environment
246
+
247
+ Add `-s|--simple` to compile the binary `755` with no setuid bit. Nothing runs as
248
+ another user; the binary exists only so that users who do not have this environment,
249
+ and cannot activate a venv or a conda environment, can still run one of its programs
250
+ by name — `gdexls`, for example:
251
+
252
+ ```bash
253
+ # Install /glade/u/apps/contrib/gdexls, a 755 binary that execs bin/gdexls:
254
+ pywrapper-install -m|--cmlink gdexls -s|--simple -d|--destdir /glade/u/apps/contrib
255
+ ```
256
+
257
+ Since there is no privilege change, sanitizing the environment would buy no security
258
+ and would only break the caller's own settings, so the caller's environment is passed
259
+ through except for `PYTHONPATH`, `PYTHONHOME` and `PYTHONSTARTUP`, which would send
260
+ the program to another environment's modules. The default `-t|--target` is
261
+ `bin/PROGRAM` rather than `bin/setuid_PROGRAM`, and no `sudo` access is required.
262
+
200
263
  ## Runtime flow
201
264
 
202
265
  ```
@@ -204,6 +267,16 @@ user runs: dsarch [args]
204
267
  | (symlink -> pywrapper, setuid bit -> EUID=gdexdata)
205
268
  pywrapper.c: execv(bin/setuid_dsarch, args)
206
269
  setuid_dsarch: calls dsarch:main() as gdexdata
270
+
271
+ user runs: gdexdrop [args]
272
+ | (dedicated 4755 binary, setuid bit -> EUID=gdexdata)
273
+ cmwrapper.c: execve(bin/setuid_gdexdrop, args, sanitized env)
274
+ setuid_gdexdrop: calls gdexdrop:main() as gdexdata
275
+
276
+ user runs: gdexls [args]
277
+ | (dedicated 755 binary, no setuid, -DCMSIMPLE)
278
+ cmwrapper.c: execv(bin/gdexls, args)
279
+ gdexls: calls gdexls:main() as the calling user
207
280
  ```
208
281
 
209
282
  ## Github
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "rda_python_setuid"
9
- version = "3.0.0"
9
+ version = "3.0.2"
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", "setuid_setup.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,120 @@
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
+ * # Compile it 755 with no setuid, to publish a program of an environment to
37
+ * # users who do not have that environment (-DCMSIMPLE):
38
+ * pywrapper-install -m|--cmlink PROGRAM -s|--simple [-d|--destdir DIR]
39
+ *
40
+ * With -DCMSIMPLE there is no privilege change, so the caller's environment is
41
+ * kept except for PYTHONPATH, PYTHONHOME and PYTHONSTARTUP, which would send the
42
+ * program to another environment's modules. Sanitizing the rest would buy no
43
+ * security and would only break the caller's own settings.
44
+ *
45
+ \***************************************************************************************/
46
+
47
+ #include <unistd.h>
48
+ #include <stdio.h>
49
+ #include <string.h>
50
+ #include <stdlib.h>
51
+
52
+ #ifndef CMPROG
53
+ #error "CMPROG must be defined at compile time, e.g. -DCMPROG=\"gdexdrop\""
54
+ #endif
55
+ #ifndef CMEXEC
56
+ #error "CMEXEC must be defined at compile time, e.g. -DCMEXEC=\"/env/bin/setuid_gdexdrop\""
57
+ #endif
58
+
59
+ #ifdef CMSIMPLE
60
+
61
+ /* Python variables of the caller that would make the wrapped program load modules
62
+ from somewhere other than its own environment. */
63
+ static const char *dropvars[] = {"PYTHONPATH", "PYTHONHOME", "PYTHONSTARTUP", NULL};
64
+
65
+ #else
66
+
67
+ #define CMPATH "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
68
+
69
+ /* Environment variables passed through from the caller. Everything else is
70
+ dropped; in particular every PYTHON*, LD_* and PGLOG path variable. */
71
+ static const char *keepvars[] = {"HOME", "USER", "LOGNAME", "TERM", "LANG", "TZ", NULL};
72
+
73
+ #endif
74
+
75
+ /* main program */
76
+ int main(int argc, char *argv[]) {
77
+ (void)argc;
78
+
79
+ #ifdef CMSIMPLE
80
+
81
+ /* No privilege change, so sanitizing the environment buys no security and can
82
+ only break the caller's own settings. Only the Python variables that would
83
+ send the program to another environment's modules are dropped. */
84
+ int i;
85
+
86
+ for(i = 0; dropvars[i] != NULL; i++) unsetenv(dropvars[i]);
87
+ argv[0] = (char *)CMPROG;
88
+ execv(CMEXEC, argv);
89
+
90
+ #else
91
+
92
+ char *envp[16];
93
+ char *value, *entry;
94
+ size_t len;
95
+ int i, n = 0;
96
+
97
+ for(i = 0; keepvars[i] != NULL; i++) {
98
+ value = getenv(keepvars[i]);
99
+ if(value == NULL) continue;
100
+ len = strlen(keepvars[i]) + strlen(value) + 2;
101
+ entry = malloc(len);
102
+ if(entry == NULL) {
103
+ perror(CMPROG ": malloc");
104
+ exit(1);
105
+ }
106
+ snprintf(entry, len, "%s=%s", keepvars[i], value);
107
+ envp[n++] = entry;
108
+ }
109
+ envp[n++] = (char *)"PATH=" CMPATH;
110
+ envp[n++] = (char *)"PYTHONNOUSERSITE=1"; /* ignore ~/.local site-packages */
111
+ envp[n] = NULL;
112
+
113
+ argv[0] = (char *)CMPROG; /* the program identifies itself by this name */
114
+ execve(CMEXEC, argv, envp);
115
+
116
+ #endif
117
+
118
+ perror(CMEXEC); /* exec only returns on error */
119
+ exit(1);
120
+ }
@@ -32,6 +32,14 @@
32
32
  # # 5. Update existing installation (recompile and reinstall all setuid binaries):
33
33
  # pywrapper-install -u [-n gdexdata] [-e $ENVHOME]
34
34
  #
35
+ # # 6. Compile a dedicated 4755 cmwrapper binary, so users outside the CommonUser
36
+ # # group can run one fixed program as CommonUser:
37
+ # pywrapper-install -m myprog [-t SCRIPT] [-d DESTDIR] [-n gdexdata] [-e $ENVHOME]
38
+ #
39
+ # # 6b. Compile it 755 with no setuid, to publish a program of this environment to
40
+ # # users who do not have the environment at all:
41
+ # pywrapper-install -m myprog -s [-t SCRIPT] [-d DESTDIR] [-e $ENVHOME]
42
+ #
35
43
  # Convention for wrapped programs:
36
44
  # The target package must register its connector entry point with a setuid_ prefix:
37
45
  # [project.scripts]
@@ -47,6 +55,7 @@
47
55
 
48
56
  import argparse
49
57
  import os
58
+ import re
50
59
  import shutil
51
60
  import subprocess
52
61
  import sys
@@ -57,9 +66,9 @@ def get_bindir():
57
66
  return os.path.dirname(os.path.abspath(sys.executable))
58
67
 
59
68
 
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')
69
+ def get_c_source(name='pywrapper.c'):
70
+ """Return path to a C source file bundled with this package."""
71
+ return os.path.join(os.path.dirname(__file__), name)
63
72
 
64
73
 
65
74
  def run(cmd):
@@ -88,7 +97,7 @@ def main():
88
97
  )
89
98
  parser.add_argument(
90
99
  '-s', '--simple', action='store_true',
91
- help="Simple install: create symlink PROGRAM -> setuid_PROGRAM, skipping setuid (use with -l/--link)"
100
+ help="Simple install, skipping setuid: symlink PROGRAM -> setuid_PROGRAM with -l/--link, or a 755 cmwrapper binary with -m/--cmlink"
92
101
  )
93
102
  group = parser.add_mutually_exclusive_group()
94
103
  group.add_argument(
@@ -103,13 +112,25 @@ def main():
103
112
  '-l', '--link', metavar='PROGRAM',
104
113
  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
114
  )
115
+ group.add_argument(
116
+ '-m', '--cmlink', metavar='PROGRAM',
117
+ help="Compile a dedicated 4755 cmwrapper binary for PROGRAM, so users outside the CommonUser group can run it (Mode 3); add -s/--simple for a 755 binary with no setuid"
118
+ )
106
119
  group.add_argument(
107
120
  '-u', '--update', action='store_true',
108
121
  help="Update an existing installation: recompile pywrapper and reinstall all pgstart_USER setuid binaries"
109
122
  )
123
+ parser.add_argument(
124
+ '-t', '--target', default=None,
125
+ help="Absolute path of the python script a cmwrapper binary execs (default: BINDIR/setuid_PROGRAM, or BINDIR/PROGRAM with -s/--simple; use with -m/--cmlink)"
126
+ )
127
+ parser.add_argument(
128
+ '-d', '--destdir', default=None,
129
+ help="Directory to install a cmwrapper binary into, such as a common area on everyone's PATH (default: BINDIR; use with -m/--cmlink)"
130
+ )
110
131
  args = parser.parse_args()
111
132
 
112
- if not (args.compile or args.pgstart or args.link or args.update):
133
+ if not (args.compile or args.pgstart or args.link or args.cmlink or args.update):
113
134
  show_usage()
114
135
 
115
136
  if args.username is None and not args.simple:
@@ -211,6 +232,45 @@ def main():
211
232
  run(['sudo', '-u', args.username, 'chmod', '4750', pywrapper])
212
233
  print("Installed: {} (setuid, owned by {})".format(pywrapper, args.username))
213
234
 
235
+ elif args.cmlink:
236
+ # Mode 3: compile a dedicated cmwrapper binary for one program and install it
237
+ # 4755, so users outside the CommonUser group can run it. The program path is
238
+ # baked in at compile time and the environment is sanitized by cmwrapper.c, so
239
+ # no symlink under another name can reach a different program.
240
+ # With -s/--simple the binary is 755 with no setuid, which publishes a program
241
+ # of this environment to users who do not have the environment at all.
242
+ if not re.match(r'^\w+$', args.cmlink):
243
+ print("Error: invalid program name '{}', expecting word characters only.".format(args.cmlink))
244
+ sys.exit(1)
245
+ if args.target:
246
+ script = args.target
247
+ else:
248
+ script = os.path.join(bindir, args.cmlink if args.simple else 'setuid_' + args.cmlink)
249
+ if not (os.path.isabs(script) and re.match(r'^[\w/.-]+$', script)):
250
+ print("Error: {} of -t/--target must be an absolute path of word characters, '/', '.' and '-'.".format(script))
251
+ sys.exit(1)
252
+ if not os.path.exists(script):
253
+ print("Error: {} not found. Install the package that provides it first.".format(script))
254
+ sys.exit(1)
255
+ destdir = args.destdir if args.destdir else bindir
256
+ target = os.path.join(destdir, args.cmlink)
257
+ if os.path.abspath(target) == os.path.abspath(script):
258
+ print("Error: the binary {} would overwrite the script it execs; give -d/--destdir or -t/--target.".format(target))
259
+ sys.exit(1)
260
+ src = get_c_source('cmwrapper.c')
261
+ src_dest = os.path.join(bindir, 'cmwrapper.c')
262
+ shutil.copy(src, src_dest)
263
+ print("Copied: {}".format(src_dest))
264
+ cmd = ['gcc', '-DCMPROG="{}"'.format(args.cmlink), '-DCMEXEC="{}"'.format(script)]
265
+ if args.simple: cmd.append('-DCMSIMPLE')
266
+ prefix = [] if args.simple else ['sudo', '-u', args.username]
267
+ run(prefix + cmd + ['-o', target, src_dest])
268
+ run(prefix + ['chmod', '755' if args.simple else '4755', target])
269
+ if args.simple:
270
+ print("Installed: {} (no setuid, execs {})".format(target, script))
271
+ else:
272
+ print("Installed: {} (setuid, owned by {}, execs {})".format(target, args.username, script))
273
+
214
274
  elif args.update:
215
275
  # Update an existing installation: recompile pywrapper and reinstall all setuid binaries
216
276
  pgstart_files = sorted(f for f in os.listdir(bindir) if f.startswith('pgstart_'))
@@ -6,11 +6,13 @@
6
6
  ( -c|--compile
7
7
  | -p|--pgstart
8
8
  | -l|--link PROGRAM|all [-s|--simple]
9
+ | -m|--cmlink PROGRAM [-s|--simple] [-t|--target SCRIPT]
10
+ [-d|--destdir DIR]
9
11
  | -u|--update )
10
12
 
11
13
  Run pywrapper-install with no arguments to display this user guide. Exactly
12
- one of -c/--compile, -p/--pgstart, -l/--link, or -u/--update must be given
13
- to perform an action.
14
+ one of -c/--compile, -p/--pgstart, -l/--link, -m/--cmlink, or -u/--update
15
+ must be given to perform an action.
14
16
 
15
17
  - Option -n or --username USERNAME
16
18
  The user name to own the setuid binary. For Mode 1 (CommonUser program),
@@ -49,6 +51,40 @@
49
51
  Use 'all' instead of a program name to scan bin/ for every setuid_*
50
52
  entry and add any missing PROGRAM -> pywrapper symlinks in one pass.
51
53
 
54
+ - Option -m or --cmlink PROGRAM
55
+ Mode 3: compile cmwrapper.c (bundled with this package) into a dedicated
56
+ binary named PROGRAM, owned by USER with chmod 4755 (setuid), so that
57
+ users who are NOT in the common user's group can run PROGRAM as
58
+ CommonUser. Unlike pywrapper, the script to exec is baked into the
59
+ binary at compile time and the environment is replaced with a fixed
60
+ whitelist, so the binary cannot be symlinked under another name to reach
61
+ a different program, and PYTHONPATH, LD_PRELOAD or the PGLOG path
62
+ variables cannot be used to run code as CommonUser. One binary is
63
+ compiled per program. Cannot be combined with the other actions.
64
+
65
+ Only wrap a program that does its own authorization and confines where
66
+ it writes, such as gdexdrop; never wrap a general purpose program such
67
+ as gdexcp, which would let any user on the machine read or overwrite
68
+ any file of the common user.
69
+
70
+ Add -s/--simple to compile the binary 755 with no setuid, which is how
71
+ a program of this environment is published to users who do not have the
72
+ environment at all, such as gdexls. There is no privilege change, so
73
+ the caller's environment is kept except for PYTHONPATH, PYTHONHOME and
74
+ PYTHONSTARTUP, which would send the program to another environment's
75
+ modules. The default -t/--target is then bin/PROGRAM, and no sudo
76
+ access to USER is needed.
77
+
78
+ - Option -t or --target SCRIPT (use with -m/--cmlink)
79
+ Absolute path of the python script the cmwrapper binary execs.
80
+ Defaults to bin/setuid_PROGRAM of the environment, or to bin/PROGRAM
81
+ with -s/--simple.
82
+
83
+ - Option -d or --destdir DIR (use with -m/--cmlink)
84
+ Directory to install the cmwrapper binary into, typically a common area
85
+ on everyone's PATH, such as /glade/u/home/gdexdata/bin. Defaults to the
86
+ bin/ directory of the environment.
87
+
52
88
  - Option -u or --update
53
89
  Update an existing installation. Discovers all pgstart_* and pywrapper
54
90
  binaries already present in bin/, saves them to a temporary update_tmp/
@@ -56,11 +92,13 @@
56
92
  reinstalls every pgstart_* binary from update_tmp. Use -n/--username to
57
93
  specify the gdex common user (default: gdexdata).
58
94
 
59
- - Option -s or --simple (use with -l/--link)
60
- Simple install: create a symlink PROGRAM -> setuid_PROGRAM directly,
61
- skipping the setuid mechanism entirely. The program runs as the current
62
- user with no privilege change. -n/--username is not required with this option.
63
- Useful for users who do not need or cannot set up the setuid wrapper.
95
+ - Option -s or --simple (use with -l/--link or -m/--cmlink)
96
+ Simple install: skip the setuid mechanism entirely, so that the program
97
+ runs as the current user with no privilege change. -n/--username is not
98
+ required with this option. With -l/--link, a symlink PROGRAM ->
99
+ setuid_PROGRAM is created for users who do not need or cannot set up the
100
+ setuid wrapper; with -m/--cmlink, a 755 cmwrapper binary is compiled for
101
+ users who do not have this environment at all.
64
102
 
65
103
  Convention for wrapped programs:
66
104
  Any Python package whose program is to be run via pywrapper must:
@@ -170,3 +208,11 @@
170
208
 
171
209
  8. Simple install of all setuid_* entries at once:
172
210
  pywrapper-install -l all -s
211
+
212
+ 9. Let users outside the gdexdata group run gdexdrop as gdexdata, installing
213
+ the 4755 binary into a common area on everyone's PATH:
214
+ pywrapper-install -m gdexdrop -d /glade/u/home/gdexdata/bin
215
+
216
+ 10. Publish gdexls of this environment to users who have no environment of
217
+ their own, with no setuid and no privilege change:
218
+ pywrapper-install -m gdexls -s -d /glade/u/apps/contrib
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_setuid
3
- Version: 3.0.0
3
+ Version: 3.0.2
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
@@ -23,7 +23,7 @@ via setuid for effective and common user names.
23
23
  user, then `execv`s a Python entry point script. This allows Python programs to run
24
24
  as a designated common user (e.g. `gdexdata`) without requiring `sudo` access.
25
25
 
26
- Two modes are supported:
26
+ Three modes are supported:
27
27
 
28
28
  - **Mode 1 (CommonUser program):** a symlink `dsarch -> pywrapper` runs `setuid_dsarch`
29
29
  as the common user.
@@ -31,6 +31,12 @@ Two modes are supported:
31
31
  runs any command as `<loginname>` via `pgstart.py`. `<loginname>` can be any
32
32
  user that belongs to the same group as `PGLOG['COMMONUSER']`. Execution is
33
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). With `-s|--simple` the same binary is installed
38
+ `755` with no setuid, which publishes ONE program of this environment to users
39
+ who have no environment of their own.
34
40
 
35
41
  Two Python entry points are packaged alongside the C wrapper:
36
42
 
@@ -213,6 +219,63 @@ pywrapper-install -l|--link dsarch -s|--simple
213
219
  pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
214
220
  ```
215
221
 
222
+ ### cmwrapper (Mode 3, for callers outside the CommonUser group)
223
+
224
+ `pywrapper` is installed `4750`, so only members of the common user's group can
225
+ execute it. That group restriction is what makes it safe for `pywrapper` to pick
226
+ the program to run from `basename(argv[0])`: anyone who can execute it can already
227
+ reach every `setuid_*` entry in `bin/` just by naming the symlink differently.
228
+
229
+ `cmwrapper` is for the opposite case — letting users who are **not** in the common
230
+ user's group run one specific program as the common user. It is installed `4755`,
231
+ i.e. executable by everyone, so nothing about what it runs may come from the caller:
232
+
233
+ - the absolute path of the script to exec, and the program name, are baked into the
234
+ binary at compile time, so it cannot be symlinked under another name to reach a
235
+ different program;
236
+ - the environment is replaced with a fixed whitelist (`HOME`, `USER`, `LOGNAME`,
237
+ `TERM`, `LANG`, `TZ`, a fixed `PATH` and `PYTHONNOUSERSITE=1`), so `PYTHONPATH`,
238
+ `PYTHONHOME`, `LD_PRELOAD`, `LD_LIBRARY_PATH` and the `PGLOG` path variables
239
+ (`DSDHOME`, `DSSHOME`, `LOGPATH`, `COMMONUSER`, ...) cannot be used to run
240
+ arbitrary code, or redirect where files are written, as the common user.
241
+
242
+ One binary is compiled per wrapped program:
243
+
244
+ ```bash
245
+ # Install bin/gdexdrop as a 4755 binary that execs bin/setuid_gdexdrop:
246
+ pywrapper-install -m|--cmlink gdexdrop
247
+
248
+ # Same, but installed into a common area already on everyone's PATH:
249
+ pywrapper-install -m|--cmlink gdexdrop -d|--destdir /glade/u/home/gdexdata/bin
250
+
251
+ # Point it at a script somewhere other than bin/setuid_gdexdrop:
252
+ pywrapper-install -m|--cmlink gdexdrop -t|--target /path/to/setuid_gdexdrop
253
+ ```
254
+
255
+ Only wrap a program that does its own authorization and confines where it writes,
256
+ such as `gdexdrop`, which checks its caller against an access list and copies only
257
+ into the requested dataset directory. Never wrap a general purpose program such as
258
+ `gdexcp`: at `4755` that would let any user on the machine read or overwrite any
259
+ file of the common user.
260
+
261
+ ### cmwrapper with no setuid, to publish a program of this environment
262
+
263
+ Add `-s|--simple` to compile the binary `755` with no setuid bit. Nothing runs as
264
+ another user; the binary exists only so that users who do not have this environment,
265
+ and cannot activate a venv or a conda environment, can still run one of its programs
266
+ by name — `gdexls`, for example:
267
+
268
+ ```bash
269
+ # Install /glade/u/apps/contrib/gdexls, a 755 binary that execs bin/gdexls:
270
+ pywrapper-install -m|--cmlink gdexls -s|--simple -d|--destdir /glade/u/apps/contrib
271
+ ```
272
+
273
+ Since there is no privilege change, sanitizing the environment would buy no security
274
+ and would only break the caller's own settings, so the caller's environment is passed
275
+ through except for `PYTHONPATH`, `PYTHONHOME` and `PYTHONSTARTUP`, which would send
276
+ the program to another environment's modules. The default `-t|--target` is
277
+ `bin/PROGRAM` rather than `bin/setuid_PROGRAM`, and no `sudo` access is required.
278
+
216
279
  ## Runtime flow
217
280
 
218
281
  ```
@@ -220,6 +283,16 @@ user runs: dsarch [args]
220
283
  | (symlink -> pywrapper, setuid bit -> EUID=gdexdata)
221
284
  pywrapper.c: execv(bin/setuid_dsarch, args)
222
285
  setuid_dsarch: calls dsarch:main() as gdexdata
286
+
287
+ user runs: gdexdrop [args]
288
+ | (dedicated 4755 binary, setuid bit -> EUID=gdexdata)
289
+ cmwrapper.c: execve(bin/setuid_gdexdrop, args, sanitized env)
290
+ setuid_gdexdrop: calls gdexdrop:main() as gdexdata
291
+
292
+ user runs: gdexls [args]
293
+ | (dedicated 755 binary, no setuid, -DCMSIMPLE)
294
+ cmwrapper.c: execv(bin/gdexls, args)
295
+ gdexls: calls gdexls:main() as the calling user
223
296
  ```
224
297
 
225
298
  ## Github
@@ -3,6 +3,7 @@ MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
5
  src/rda_python_setuid/__init__.py
6
+ src/rda_python_setuid/cmwrapper.c
6
7
  src/rda_python_setuid/install.py
7
8
  src/rda_python_setuid/install.usg
8
9
  src/rda_python_setuid/pgstart.py