rda-python-setuid 3.0.0__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 (22) hide show
  1. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/MANIFEST.in +1 -0
  2. {rda_python_setuid-3.0.0/src/rda_python_setuid.egg-info → rda_python_setuid-3.0.1}/PKG-INFO +50 -2
  3. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/README.md +49 -1
  4. {rda_python_setuid-3.0.0 → 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.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid/install.py +48 -4
  7. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid/install.usg +32 -2
  8. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1/src/rda_python_setuid.egg-info}/PKG-INFO +50 -2
  9. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/SOURCES.txt +1 -0
  10. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/LICENSE +0 -0
  11. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/setup.cfg +0 -0
  12. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid/__init__.py +0 -0
  13. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid/pgstart.py +0 -0
  14. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid/pywrapper.c +0 -0
  15. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid/pywrapper.py +0 -0
  16. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid/setuid_setup.usg +0 -0
  17. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid/setup_guide.py +0 -0
  18. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/dependency_links.txt +0 -0
  19. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/entry_points.txt +0 -0
  20. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/requires.txt +0 -0
  21. {rda_python_setuid-3.0.0 → rda_python_setuid-3.0.1}/src/rda_python_setuid.egg-info/top_level.txt +0 -0
  22. {rda_python_setuid-3.0.0 → 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
@@ -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.1
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,10 @@ 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).
34
38
 
35
39
  Two Python entry points are packaged alongside the C wrapper:
36
40
 
@@ -213,6 +217,45 @@ pywrapper-install -l|--link dsarch -s|--simple
213
217
  pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
214
218
  ```
215
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
+
216
259
  ## Runtime flow
217
260
 
218
261
  ```
@@ -220,6 +263,11 @@ user runs: dsarch [args]
220
263
  | (symlink -> pywrapper, setuid bit -> EUID=gdexdata)
221
264
  pywrapper.c: execv(bin/setuid_dsarch, args)
222
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
223
271
  ```
224
272
 
225
273
  ## 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,10 @@ 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).
18
22
 
19
23
  Two Python entry points are packaged alongside the C wrapper:
20
24
 
@@ -197,6 +201,45 @@ pywrapper-install -l|--link dsarch -s|--simple
197
201
  pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
198
202
  ```
199
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
+
200
243
  ## Runtime flow
201
244
 
202
245
  ```
@@ -204,6 +247,11 @@ user runs: dsarch [args]
204
247
  | (symlink -> pywrapper, setuid bit -> EUID=gdexdata)
205
248
  pywrapper.c: execv(bin/setuid_dsarch, args)
206
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
207
255
  ```
208
256
 
209
257
  ## 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.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", "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,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
+ }
@@ -32,6 +32,10 @@
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
+ #
35
39
  # Convention for wrapped programs:
36
40
  # The target package must register its connector entry point with a setuid_ prefix:
37
41
  # [project.scripts]
@@ -47,6 +51,7 @@
47
51
 
48
52
  import argparse
49
53
  import os
54
+ import re
50
55
  import shutil
51
56
  import subprocess
52
57
  import sys
@@ -57,9 +62,9 @@ def get_bindir():
57
62
  return os.path.dirname(os.path.abspath(sys.executable))
58
63
 
59
64
 
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')
65
+ def get_c_source(name='pywrapper.c'):
66
+ """Return path to a C source file bundled with this package."""
67
+ return os.path.join(os.path.dirname(__file__), name)
63
68
 
64
69
 
65
70
  def run(cmd):
@@ -103,13 +108,25 @@ def main():
103
108
  '-l', '--link', metavar='PROGRAM',
104
109
  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
110
  )
111
+ group.add_argument(
112
+ '-m', '--cmlink', metavar='PROGRAM',
113
+ help="Compile a dedicated 4755 cmwrapper binary for PROGRAM, so users outside the CommonUser group can run it (Mode 3)"
114
+ )
106
115
  group.add_argument(
107
116
  '-u', '--update', action='store_true',
108
117
  help="Update an existing installation: recompile pywrapper and reinstall all pgstart_USER setuid binaries"
109
118
  )
119
+ parser.add_argument(
120
+ '-t', '--target', default=None,
121
+ help="Absolute path of the python script a cmwrapper binary execs (default: BINDIR/setuid_PROGRAM; use with -m/--cmlink)"
122
+ )
123
+ parser.add_argument(
124
+ '-d', '--destdir', default=None,
125
+ help="Directory to install a cmwrapper binary into, such as a common area on everyone's PATH (default: BINDIR; use with -m/--cmlink)"
126
+ )
110
127
  args = parser.parse_args()
111
128
 
112
- if not (args.compile or args.pgstart or args.link or args.update):
129
+ if not (args.compile or args.pgstart or args.link or args.cmlink or args.update):
113
130
  show_usage()
114
131
 
115
132
  if args.username is None and not args.simple:
@@ -211,6 +228,33 @@ def main():
211
228
  run(['sudo', '-u', args.username, 'chmod', '4750', pywrapper])
212
229
  print("Installed: {} (setuid, owned by {})".format(pywrapper, args.username))
213
230
 
231
+ elif args.cmlink:
232
+ # Mode 3: compile a dedicated cmwrapper binary for one program and install it
233
+ # 4755, so users outside the CommonUser group can run it. The program path is
234
+ # baked in at compile time and the environment is sanitized by cmwrapper.c, so
235
+ # no symlink under another name can reach a different program.
236
+ if not re.match(r'^\w+$', args.cmlink):
237
+ print("Error: invalid program name '{}', expecting word characters only.".format(args.cmlink))
238
+ sys.exit(1)
239
+ script = args.target if args.target else os.path.join(bindir, 'setuid_' + args.cmlink)
240
+ if not (os.path.isabs(script) and re.match(r'^[\w/.-]+$', script)):
241
+ print("Error: {} of -t/--target must be an absolute path of word characters, '/', '.' and '-'.".format(script))
242
+ sys.exit(1)
243
+ if not os.path.exists(script):
244
+ print("Error: {} not found. Install the package that provides it first.".format(script))
245
+ sys.exit(1)
246
+ destdir = args.destdir if args.destdir else bindir
247
+ target = os.path.join(destdir, args.cmlink)
248
+ src = get_c_source('cmwrapper.c')
249
+ src_dest = os.path.join(bindir, 'cmwrapper.c')
250
+ shutil.copy(src, src_dest)
251
+ print("Copied: {}".format(src_dest))
252
+ run(['sudo', '-u', args.username, 'gcc',
253
+ '-DCMPROG="{}"'.format(args.cmlink), '-DCMEXEC="{}"'.format(script),
254
+ '-o', target, src_dest])
255
+ run(['sudo', '-u', args.username, 'chmod', '4755', target])
256
+ print("Installed: {} (setuid, owned by {}, execs {})".format(target, args.username, script))
257
+
214
258
  elif args.update:
215
259
  # Update an existing installation: recompile pywrapper and reinstall all setuid binaries
216
260
  pgstart_files = sorted(f for f in os.listdir(bindir) if f.startswith('pgstart_'))
@@ -6,11 +6,12 @@
6
6
  ( -c|--compile
7
7
  | -p|--pgstart
8
8
  | -l|--link PROGRAM|all [-s|--simple]
9
+ | -m|--cmlink PROGRAM [-t|--target SCRIPT] [-d|--destdir DIR]
9
10
  | -u|--update )
10
11
 
11
12
  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.
13
+ one of -c/--compile, -p/--pgstart, -l/--link, -m/--cmlink, or -u/--update
14
+ must be given to perform an action.
14
15
 
15
16
  - Option -n or --username USERNAME
16
17
  The user name to own the setuid binary. For Mode 1 (CommonUser program),
@@ -49,6 +50,31 @@
49
50
  Use 'all' instead of a program name to scan bin/ for every setuid_*
50
51
  entry and add any missing PROGRAM -> pywrapper symlinks in one pass.
51
52
 
53
+ - Option -m or --cmlink PROGRAM
54
+ Mode 3: compile cmwrapper.c (bundled with this package) into a dedicated
55
+ binary named PROGRAM, owned by USER with chmod 4755 (setuid), so that
56
+ users who are NOT in the common user's group can run PROGRAM as
57
+ CommonUser. Unlike pywrapper, the script to exec is baked into the
58
+ binary at compile time and the environment is replaced with a fixed
59
+ whitelist, so the binary cannot be symlinked under another name to reach
60
+ a different program, and PYTHONPATH, LD_PRELOAD or the PGLOG path
61
+ variables cannot be used to run code as CommonUser. One binary is
62
+ compiled per program. Cannot be combined with the other actions.
63
+
64
+ Only wrap a program that does its own authorization and confines where
65
+ it writes, such as gdexdrop; never wrap a general purpose program such
66
+ as gdexcp, which would let any user on the machine read or overwrite
67
+ any file of the common user.
68
+
69
+ - Option -t or --target SCRIPT (use with -m/--cmlink)
70
+ Absolute path of the python script the cmwrapper binary execs.
71
+ Defaults to bin/setuid_PROGRAM of the environment.
72
+
73
+ - Option -d or --destdir DIR (use with -m/--cmlink)
74
+ Directory to install the cmwrapper binary into, typically a common area
75
+ on everyone's PATH, such as /glade/u/home/gdexdata/bin. Defaults to the
76
+ bin/ directory of the environment.
77
+
52
78
  - Option -u or --update
53
79
  Update an existing installation. Discovers all pgstart_* and pywrapper
54
80
  binaries already present in bin/, saves them to a temporary update_tmp/
@@ -170,3 +196,7 @@
170
196
 
171
197
  8. Simple install of all setuid_* entries at once:
172
198
  pywrapper-install -l all -s
199
+
200
+ 9. Let users outside the gdexdata group run gdexdrop as gdexdata, installing
201
+ the 4755 binary into a common area on everyone's PATH:
202
+ pywrapper-install -m gdexdrop -d /glade/u/home/gdexdata/bin
@@ -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.1
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,10 @@ 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).
34
38
 
35
39
  Two Python entry points are packaged alongside the C wrapper:
36
40
 
@@ -213,6 +217,45 @@ pywrapper-install -l|--link dsarch -s|--simple
213
217
  pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
214
218
  ```
215
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
+
216
259
  ## Runtime flow
217
260
 
218
261
  ```
@@ -220,6 +263,11 @@ user runs: dsarch [args]
220
263
  | (symlink -> pywrapper, setuid bit -> EUID=gdexdata)
221
264
  pywrapper.c: execv(bin/setuid_dsarch, args)
222
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
223
271
  ```
224
272
 
225
273
  ## 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