rda-python-setuid 1.0.9__py3-none-any.whl → 3.0.1__py3-none-any.whl

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.
@@ -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
+ }
@@ -15,17 +15,27 @@
15
15
  # pywrapper-install
16
16
  #
17
17
  # # 1. Compile and install pywrapper (run once per environment):
18
- # pywrapper-install -c [-u gdexdata] [-e $ENVHOME]
18
+ # pywrapper-install -c [-n gdexdata] [-e $ENVHOME]
19
19
  #
20
20
  # # 2. Create pgstart_USER entry so USER can run commands as themselves:
21
- # pywrapper-install -p [-u zji] [-e $ENVHOME]
21
+ # pywrapper-install -p [-n zji] [-e $ENVHOME]
22
22
  #
23
23
  # # 3. Create a symlink so a program runs as CommonUser via pywrapper (setuid):
24
- # pywrapper-install -l myprog [-u gdexdata] [-e $ENVHOME]
24
+ # pywrapper-install -l myprog [-n gdexdata] [-e $ENVHOME]
25
+ #
26
+ # # 3b. Auto-link all discovered setuid_* entries that are not yet linked:
27
+ # pywrapper-install -l all [-e $ENVHOME]
25
28
  #
26
29
  # # 4. Simple install: symlink PROGRAM -> setuid_PROGRAM (no setuid, runs as current user):
27
30
  # pywrapper-install -l myprog -s [-e $ENVHOME]
28
31
  #
32
+ # # 5. Update existing installation (recompile and reinstall all setuid binaries):
33
+ # pywrapper-install -u [-n gdexdata] [-e $ENVHOME]
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
+ #
29
39
  # Convention for wrapped programs:
30
40
  # The target package must register its connector entry point with a setuid_ prefix:
31
41
  # [project.scripts]
@@ -41,6 +51,7 @@
41
51
 
42
52
  import argparse
43
53
  import os
54
+ import re
44
55
  import shutil
45
56
  import subprocess
46
57
  import sys
@@ -51,9 +62,9 @@ def get_bindir():
51
62
  return os.path.dirname(os.path.abspath(sys.executable))
52
63
 
53
64
 
54
- def get_c_source():
55
- """Return path to pywrapper.c bundled with this package."""
56
- 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)
57
68
 
58
69
 
59
70
  def run(cmd):
@@ -77,7 +88,7 @@ def main():
77
88
  help="Path to the venv root directory containing bin/ (default: parent of the current Python executable's bin/ dir)"
78
89
  )
79
90
  parser.add_argument(
80
- '-u', '--user', default=None,
91
+ '-n', '--username', default=None,
81
92
  help="User name to own the setuid binary (default: current login user for -p/--pgstart, gdexdata otherwise)"
82
93
  )
83
94
  parser.add_argument(
@@ -95,57 +106,117 @@ def main():
95
106
  )
96
107
  group.add_argument(
97
108
  '-l', '--link', metavar='PROGRAM',
98
- help="Create symlink PROGRAM -> pywrapper for running a fixed program as CommonUser (Mode 1)"
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"
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
+ )
115
+ group.add_argument(
116
+ '-u', '--update', action='store_true',
117
+ help="Update an existing installation: recompile pywrapper and reinstall all pgstart_USER setuid binaries"
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)"
99
126
  )
100
127
  args = parser.parse_args()
101
128
 
102
- if not (args.compile or args.pgstart or args.link):
129
+ if not (args.compile or args.pgstart or args.link or args.cmlink or args.update):
103
130
  show_usage()
104
131
 
105
- if args.user is None and not args.simple:
132
+ if args.username is None and not args.simple:
106
133
  import pwd
107
134
  if args.pgstart:
108
- args.user = pwd.getpwuid(os.getuid()).pw_name
135
+ args.username = pwd.getpwuid(os.getuid()).pw_name
109
136
  else:
110
- args.user = 'gdexdata'
137
+ args.username = 'gdexdata'
111
138
 
112
139
  bindir = os.path.join(args.envhome, 'bin') if args.envhome else get_bindir()
113
140
  pywrapper = os.path.join(bindir, 'pywrapper')
114
141
 
115
142
  if args.link:
116
- target = os.path.join(bindir, args.link)
117
- script = os.path.join(bindir, 'setuid_{}'.format(args.link))
118
- if not os.path.exists(script):
119
- print("Error: {} not found. Install the package that provides it first.".format(script))
120
- sys.exit(1)
121
- if args.simple:
122
- # Simple install: symlink PROGRAM -> setuid_PROGRAM, no setuid, runs as current user.
123
- if os.path.lexists(target):
124
- print("Already exists: {}".format(target))
125
- else:
126
- os.symlink(script, target)
127
- print("Created: {} -> setuid_{}".format(target, args.link))
143
+ # For appname -> pywrapper links, run `ln -s` via pgstart_<commonuser> so
144
+ # the resulting symlink is owned by the common user (pywrapper owner).
145
+ pgstart_common = None
146
+ if not args.simple and os.path.exists(pywrapper):
147
+ import pwd
148
+ common_user = pwd.getpwuid(os.stat(pywrapper).st_uid).pw_name
149
+ pgstart_common = os.path.join(bindir, 'pgstart_' + common_user)
150
+ if not os.path.exists(pgstart_common):
151
+ print("Error: {} not found. Run pywrapper-install --pgstart --username {} first.".format(pgstart_common, common_user))
152
+ sys.exit(1)
153
+
154
+ if args.link.lower() == 'all':
155
+ # Discover all setuid_* entries in bindir and link any that are missing
156
+ appnames = sorted(
157
+ f[len('setuid_'):] for f in os.listdir(bindir) if f.startswith('setuid_')
158
+ )
159
+ if not appnames:
160
+ print("No setuid_* entries found in {}".format(bindir))
161
+ for appname in appnames:
162
+ target = os.path.join(bindir, appname)
163
+ script = os.path.join(bindir, 'setuid_' + appname)
164
+ if args.simple:
165
+ if os.path.lexists(target):
166
+ print("Already exists: {}".format(target))
167
+ else:
168
+ os.symlink(script, target)
169
+ print("Created: {} -> setuid_{}".format(target, appname))
170
+ else:
171
+ if os.path.lexists(target):
172
+ print("Already exists: {}".format(target))
173
+ else:
174
+ run([pgstart_common, 'ln', '-s', pywrapper, target])
175
+ print("Created: {} -> pywrapper".format(target))
128
176
  else:
129
- # Mode 1: symlink PROGRAM -> pywrapper. setuid_PROGRAM is left with its
130
- # default ownership/permissions so it can be loaded and executed normally.
131
- if os.path.lexists(target):
132
- print("Already exists: {}".format(target))
177
+ target = os.path.join(bindir, args.link)
178
+ script = os.path.join(bindir, 'setuid_{}'.format(args.link))
179
+ if not os.path.exists(script):
180
+ print("Error: {} not found. Install the package that provides it first.".format(script))
181
+ sys.exit(1)
182
+ if args.simple:
183
+ # Simple install: symlink PROGRAM -> setuid_PROGRAM, no setuid, runs as current user.
184
+ if os.path.lexists(target):
185
+ print("Already exists: {}".format(target))
186
+ else:
187
+ os.symlink(script, target)
188
+ print("Created: {} -> setuid_{}".format(target, args.link))
133
189
  else:
134
- os.symlink(pywrapper, target)
135
- print("Created: {} -> pywrapper".format(target))
190
+ # Mode 1: symlink PROGRAM -> pywrapper. setuid_PROGRAM is left with its
191
+ # default ownership/permissions so it can be loaded and executed normally.
192
+ if os.path.lexists(target):
193
+ print("Already exists: {}".format(target))
194
+ else:
195
+ run([pgstart_common, 'ln', '-s', pywrapper, target])
196
+ print("Created: {} -> pywrapper".format(target))
136
197
 
137
198
  elif args.pgstart:
138
- # Mode 2: copy pywrapper to pgstart_USER with setuid owned by USER
199
+ # Mode 2: create pgstart_USER with setuid owned by USER.
200
+ # When USER already owns pywrapper (i.e. the common user), pywrapper is
201
+ # already setuid owned by USER, so a symlink is sufficient; otherwise
202
+ # copy pywrapper and chmod 4750 as USER.
139
203
  if not os.path.exists(pywrapper):
140
- print("Error: {} not found. Run pywrapper-install --user COMMONUSER first.".format(pywrapper))
204
+ print("Error: {} not found. Run pywrapper-install --compile --username COMMONUSER first.".format(pywrapper))
141
205
  sys.exit(1)
142
- target = os.path.join(bindir, 'pgstart_{}'.format(args.user))
206
+ target = os.path.join(bindir, 'pgstart_{}'.format(args.username))
143
207
  import pwd
144
- curuser = pwd.getpwuid(os.getuid()).pw_name
145
- sudo_prefix = [] if curuser == args.user else ['sudo', '-u', args.user]
146
- run(sudo_prefix + ['cp', pywrapper, target])
147
- run(sudo_prefix + ['chmod', '4750', target])
148
- print("Installed: {} (setuid, owned by {})".format(target, args.user))
208
+ pywrapper_owner = pwd.getpwuid(os.stat(pywrapper).st_uid).pw_name
209
+ if args.username == pywrapper_owner:
210
+ if os.path.lexists(target):
211
+ os.remove(target)
212
+ os.symlink(pywrapper, target)
213
+ print("Linked: {} -> pywrapper (setuid, owned by {})".format(target, args.username))
214
+ else:
215
+ curuser = pwd.getpwuid(os.getuid()).pw_name
216
+ sudo_prefix = [] if curuser == args.username else ['sudo', '-u', args.username]
217
+ run(sudo_prefix + ['cp', pywrapper, target])
218
+ run(sudo_prefix + ['chmod', '4750', target])
219
+ print("Installed: {} (setuid, owned by {})".format(target, args.username))
149
220
 
150
221
  elif args.compile:
151
222
  # Compile pywrapper.c and install pywrapper with setuid
@@ -153,9 +224,107 @@ def main():
153
224
  src_dest = os.path.join(bindir, 'pywrapper.c')
154
225
  shutil.copy(src, src_dest)
155
226
  print("Copied: {}".format(src_dest))
156
- run(['sudo', '-u', args.user, 'gcc', '-o', pywrapper, src_dest])
157
- run(['sudo', '-u', args.user, 'chmod', '4750', pywrapper])
158
- print("Installed: {} (setuid, owned by {})".format(pywrapper, args.user))
227
+ run(['sudo', '-u', args.username, 'gcc', '-o', pywrapper, src_dest])
228
+ run(['sudo', '-u', args.username, 'chmod', '4750', pywrapper])
229
+ print("Installed: {} (setuid, owned by {})".format(pywrapper, args.username))
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
+
258
+ elif args.update:
259
+ # Update an existing installation: recompile pywrapper and reinstall all setuid binaries
260
+ pgstart_files = sorted(f for f in os.listdir(bindir) if f.startswith('pgstart_'))
261
+ if not pgstart_files:
262
+ print("Error: No pgstart_* binaries found in {}".format(bindir))
263
+ sys.exit(1)
264
+ if not os.path.exists(pywrapper):
265
+ print("Error: {} not found.".format(pywrapper))
266
+ sys.exit(1)
267
+
268
+ gdexuser = args.username
269
+ pgstart_gdexdata = os.path.join(bindir, 'pgstart_' + gdexuser)
270
+ if not os.path.exists(pgstart_gdexdata):
271
+ print("Error: {} not found.".format(pgstart_gdexdata))
272
+ sys.exit(1)
273
+
274
+ update_tmp = os.path.join(bindir, 'update_tmp')
275
+ os.makedirs(update_tmp, exist_ok=True)
276
+ print("Created: {}".format(update_tmp))
277
+
278
+ # Symlink pgstart.py into update_tmp so pywrapper instances running from
279
+ # update_tmp can find it via the fpath/pgstart.py fallback lookup.
280
+ bindir_pgstart_py = os.path.join(bindir, 'pgstart.py')
281
+ update_pgstart_py = os.path.join(update_tmp, 'pgstart.py')
282
+ if not os.path.lexists(update_pgstart_py):
283
+ os.symlink(bindir_pgstart_py, update_pgstart_py)
284
+ print("Linked: {} -> {}".format(update_pgstart_py, bindir_pgstart_py))
285
+
286
+ # Copy each non-gdex pgstart_USERNAME into update_tmp using itself, then chmod 4750
287
+ for fname in pgstart_files:
288
+ username = fname[len('pgstart_'):]
289
+ if username == gdexuser:
290
+ continue
291
+ src_pgstart = os.path.join(bindir, fname)
292
+ dst_pgstart = os.path.join(update_tmp, fname)
293
+ run([src_pgstart, 'cp', src_pgstart, update_tmp + '/'])
294
+ run([src_pgstart, 'chmod', '4750', dst_pgstart])
295
+
296
+ # Copy pywrapper into update_tmp as gdexdata, chmod, then hardlink as pgstart_gdexdata
297
+ update_pywrapper = os.path.join(update_tmp, 'pywrapper')
298
+ update_pgstart_gdexdata = os.path.join(update_tmp, 'pgstart_' + gdexuser)
299
+ run([pgstart_gdexdata, 'cp', pywrapper, update_tmp + '/'])
300
+ run([pgstart_gdexdata, 'chmod', '4750', update_pywrapper])
301
+ run([pgstart_gdexdata, 'ln', update_pywrapper, update_pgstart_gdexdata])
302
+
303
+ # Compile new pywrapper using update_tmp/pgstart_gdexdata
304
+ src = get_c_source()
305
+ src_dest = os.path.join(bindir, 'pywrapper.c')
306
+ shutil.copy(src, src_dest)
307
+ print("Copied: {}".format(src_dest))
308
+ run([update_pgstart_gdexdata, 'gcc', '-o', pywrapper, src_dest])
309
+ run([update_pgstart_gdexdata, 'chmod', '4750', pywrapper])
310
+ print("Compiled: {} (setuid, owned by {})".format(pywrapper, gdexuser))
311
+
312
+ # Recreate each pgstart_* in bindir using the corresponding update_tmp/pgstart_*
313
+ for fname in sorted(f for f in os.listdir(update_tmp) if f.startswith('pgstart_')):
314
+ username = fname[len('pgstart_'):]
315
+ update_pgstart = os.path.join(update_tmp, fname)
316
+ target = os.path.join(bindir, fname)
317
+ if username == gdexuser:
318
+ run([update_pgstart_gdexdata, 'ln', '-sf', pywrapper, target])
319
+ print("Linked: {} -> pywrapper (setuid, owned by {})".format(target, gdexuser))
320
+ else:
321
+ run([update_pgstart, 'cp', pywrapper, target])
322
+ run([update_pgstart, 'chmod', '4750', target])
323
+ print("Updated: {} (setuid, owned by {})".format(target, username))
324
+
325
+ # Clean up the temporary working directory
326
+ shutil.rmtree(update_tmp)
327
+ print("Removed: {}".format(update_tmp))
159
328
 
160
329
 
161
330
  if __name__ == '__main__': main()
@@ -2,16 +2,18 @@
2
2
  that execute Python scripts as a common or effective user via the setuid mechanism.
3
3
  Must be run inside the target Python virtual environment.
4
4
 
5
- Usage: pywrapper-install [-u|--user USER] [-e|--envhome ENVHOME]
5
+ Usage: pywrapper-install [-n|--username USERNAME] [-e|--envhome ENVHOME]
6
6
  ( -c|--compile
7
7
  | -p|--pgstart
8
- | -l|--link PROGRAM [-s|--simple] )
8
+ | -l|--link PROGRAM|all [-s|--simple]
9
+ | -m|--cmlink PROGRAM [-t|--target SCRIPT] [-d|--destdir DIR]
10
+ | -u|--update )
9
11
 
10
12
  Run pywrapper-install with no arguments to display this user guide. Exactly
11
- one of -c/--compile, -p/--pgstart, or -l/--link must be given to perform an
12
- action.
13
+ one of -c/--compile, -p/--pgstart, -l/--link, -m/--cmlink, or -u/--update
14
+ must be given to perform an action.
13
15
 
14
- - Option -u or --user USER
16
+ - Option -n or --username USERNAME
15
17
  The user name to own the setuid binary. For Mode 1 (CommonUser program),
16
18
  this is the common user (e.g. gdexdata). For Mode 2 (pgstart), this is
17
19
  the specialist user (e.g. zji). Defaults to 'gdexdata' unless -p/--pgstart
@@ -31,20 +33,59 @@
31
33
 
32
34
  - Option -p or --pgstart
33
35
  Mode 2: copy the compiled pywrapper binary to pgstart_USER (owned by USER,
34
- chmod 4750). Allows USER to run arbitrary commands as themselves via the
35
- setuid wrapper. Cannot be combined with -c/--compile or -l/--link.
36
-
37
- - Option -l or --link PROGRAM
36
+ chmod 4750). Allows USER (any login user in the same group as the
37
+ common user PGLOG['COMMONUSER']) to run arbitrary commands as themselves
38
+ via the setuid wrapper. Two paths for setting up pgstart_USER:
39
+ (a) if PGLOG['ADMINUSER'] (default zji) can 'sudo -u USER', the admin
40
+ runs 'pywrapper-install -p -n USER' on behalf of USER; or
41
+ (b) USER runs the same command themselves (no sudo required).
42
+ Cannot be combined with -c/--compile or -l/--link.
43
+
44
+ - Option -l or --link PROGRAM|all
38
45
  Mode 1: create a symlink PROGRAM -> pywrapper in the bin/ directory so
39
46
  that running PROGRAM invokes the setuid wrapper, which then execs
40
47
  setuid_PROGRAM as CommonUser. setuid_PROGRAM keeps its default
41
48
  ownership and permissions. Cannot be combined with -c/--compile or
42
49
  -p/--pgstart.
50
+ Use 'all' instead of a program name to scan bin/ for every setuid_*
51
+ entry and add any missing PROGRAM -> pywrapper symlinks in one pass.
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
+
78
+ - Option -u or --update
79
+ Update an existing installation. Discovers all pgstart_* and pywrapper
80
+ binaries already present in bin/, saves them to a temporary update_tmp/
81
+ subdirectory, recompiles pywrapper using update_tmp/pgstart_COMMONUSER, and
82
+ reinstalls every pgstart_* binary from update_tmp. Use -n/--username to
83
+ specify the gdex common user (default: gdexdata).
43
84
 
44
85
  - Option -s or --simple (use with -l/--link)
45
86
  Simple install: create a symlink PROGRAM -> setuid_PROGRAM directly,
46
87
  skipping the setuid mechanism entirely. The program runs as the current
47
- user with no privilege change. -u/--user is not required with this option.
88
+ user with no privilege change. -n/--username is not required with this option.
48
89
  Useful for users who do not need or cannot set up the setuid wrapper.
49
90
 
50
91
  Convention for wrapped programs:
@@ -111,10 +152,14 @@
111
152
  pywrapper-install -c
112
153
 
113
154
  3. Wire up each program as a setuid entry:
114
- pywrapper-install -l dsarch
155
+ pywrapper-install -l dsarch # one program
156
+ pywrapper-install -l all # or all setuid_* entries at once
115
157
 
116
- 4. Optionally, allow a specialist to run commands as themselves:
117
- pywrapper-install -p
158
+ 4. Optionally, install a pgstart_<loginname> binary so <loginname> (any
159
+ user in the same group as PGLOG['COMMONUSER']) can run commands as
160
+ themselves. Either PGLOG['ADMINUSER'] (default zji, if it has
161
+ 'sudo -u <loginname>'), or <loginname> directly, runs:
162
+ pywrapper-install -p -n <loginname>
118
163
 
119
164
  Option B - Simple install (no sudo required, runs as current user):
120
165
 
@@ -122,7 +167,8 @@
122
167
  pip install rda_python_dsarch
123
168
 
124
169
  2. Create a direct symlink to the connector script:
125
- pywrapper-install -l dsarch -s
170
+ pywrapper-install -l dsarch -s # one program
171
+ pywrapper-install -l all -s # or all setuid_* entries at once
126
172
 
127
173
  Examples:
128
174
 
@@ -135,8 +181,22 @@
135
181
  3. Wire up dsarch to run as gdexdata via pywrapper:
136
182
  pywrapper-install -l dsarch
137
183
 
138
- 4. Allow specialist zji to run commands as themselves via pgstart:
139
- pywrapper-install -p -u zji
184
+ 4. Wire up all setuid_* entries at once:
185
+ pywrapper-install -l all
186
+
187
+ 5. Install pgstart_zji so login user zji can run commands as themselves
188
+ via pgstart (run by ADMINUSER with 'sudo -u zji' available, or by zji):
189
+ pywrapper-install -p -n zji
140
190
 
141
- 5. Simple install of dsarch for a user who does not need setuid:
191
+ 6. Update an existing installation (recompile and reinstall all setuid binaries):
192
+ pywrapper-install -u
193
+
194
+ 7. Simple install of dsarch for a user who does not need setuid:
142
195
  pywrapper-install -l dsarch -s
196
+
197
+ 8. Simple install of all setuid_* entries at once:
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
@@ -40,7 +40,7 @@ def main():
40
40
  pglog = PgLOG()
41
41
  permit = False
42
42
  pglog.PGLOG['LOGFILE'] = "pgstart.log"
43
- aname = PgLOG.get_command()
43
+ aname = pglog.get_command()
44
44
  bckgrd = False
45
45
  workdir = None
46
46
  argv = sys.argv[1:]
@@ -49,7 +49,7 @@ def main():
49
49
  euid = pglog.PGLOG['EUID']
50
50
  ruser = pwd.getpwuid(ruid).pw_name
51
51
  euser = pwd.getpwuid(euid).pw_name
52
- if ruser == euser or ruser == pglog.PGLOG['GDEXUSER'] or euser == pglog.PGLOG['GDEXUSER']: permit = True
52
+ if ruser in [pglog.PGLOG['ADMINUSER'], euser, pglog.PGLOG['COMMONUSER']] or euser == pglog.PGLOG['COMMONUSER']: permit = True
53
53
  pglog.set_suid(euid)
54
54
 
55
55
  while argv:
@@ -69,7 +69,7 @@ def main():
69
69
  print("* Your Login Name is {}({}) & Effective User Name is {}({}).".format(ruser, ruid, euser, euid))
70
70
  print("* Pass a command or options -(bg|fg|cwd|env|inc|plg) to run '{}'.".format(aname))
71
71
  if not permit:
72
- print("* You must be '{}' or '{}' to execute a command as user '{}'.".format(euser, pglog.PGLOG['GDEXUSER'], euser))
72
+ print("* You must be '{}' or '{}' to execute a command as user '{}'.".format(euser, pglog.PGLOG['COMMONUSER'], euser))
73
73
  print("********************************************************************")
74
74
  sys.exit(0)
75
75
 
@@ -32,6 +32,8 @@ def main():
32
32
  -plg -- print PGLOG variables and exit
33
33
  """
34
34
  pglog = PgLOG()
35
+ from rda_python_setuid.setup_guide import show_setup_guide
36
+ show_setup_guide(pglog, 'rda_python_setuid', ['pywrapper'])
35
37
  pglog.set_suid(pglog.PGLOG['EUID'])
36
38
  inc = True
37
39
  print("********************************************************************")
@@ -0,0 +1,75 @@
1
+
2
+ {PKGNAME} - Setuid Setup Guide
3
+ ===============================
4
+
5
+ The following programs must run as the common user 'gdexdata' via the
6
+ rda_python_setuid mechanism:
7
+
8
+ {APPNAMES}
9
+
10
+ rda_python_setuid is installed automatically as a dependency.
11
+
12
+ If you are seeing this message after running a setuid_ connector script
13
+ directly, the setuid wrapper has not been set up yet. Follow the steps
14
+ below.
15
+
16
+ Run 'pywrapper-install' with no arguments for the full pywrapper user guide.
17
+
18
+ Environment Setup
19
+ -----------------
20
+
21
+ Option A - Python venv (DECS machines):
22
+ python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
23
+ source $ENVHOME/bin/activate
24
+
25
+ Option B - Conda (DAV/Casper):
26
+ conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
27
+ conda activate $ENVHOME
28
+
29
+ Full Setuid Setup (requires sudo access to gdexdata)
30
+ ----------------------------------------------------
31
+
32
+ # 1. Install the target package (pulls in rda_python_setuid automatically):
33
+ pip install {PKGNAME}
34
+
35
+ # 2. Compile the pywrapper C binary (once per environment):
36
+ pywrapper-install -c|--compile -n|--username gdexdata
37
+
38
+ # 3. Wire up each program as a setuid entry (specify name or use 'all'):
39
+ pywrapper-install -l|--link <program>
40
+ pywrapper-install -l|--link all # auto-link every setuid_* entry not yet linked
41
+
42
+ # 4. Optionally, install a pgstart_<loginname> binary so <loginname>
43
+ # (any user in the same group as PGLOG['COMMONUSER']) can run commands
44
+ # as themselves via the setuid wrapper. Same command in both cases,
45
+ # only the invoker differs:
46
+ #
47
+ # 4a. If PGLOG['ADMINUSER'] (default zji) can 'sudo -u <loginname>',
48
+ # the admin sets it up on the user's behalf:
49
+ pywrapper-install -p|--pgstart -n|--username <loginname>
50
+ #
51
+ # 4b. Otherwise <loginname> runs the same command themselves (no sudo
52
+ # from ADMINUSER required, since they already are <loginname>):
53
+ pywrapper-install -p|--pgstart -n|--username <loginname>
54
+
55
+ Update Existing Installation (no sudo required)
56
+ -----------------------------------------------
57
+
58
+ When the package is upgraded and a new pywrapper.c is bundled, use
59
+ -u/--update to recompile and reinstall all setuid binaries without
60
+ needing sudo. The existing pgstart_* binaries in bin/ are used to
61
+ perform the privileged operations:
62
+
63
+ pywrapper-install -u|--update [-n|--username gdexdata] [-e|--envhome $ENVHOME]
64
+
65
+ Simple Install (no sudo required, runs as current user)
66
+ -------------------------------------------------------
67
+
68
+ Users who do not need the setuid mechanism can create direct symlinks
69
+ from <name> to setuid_<name>:
70
+
71
+ pywrapper-install -l|--link <program> -s|--simple
72
+ pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
73
+
74
+ The programs run as the current user with no privilege change.
75
+
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ ##################################################################################
4
+ #
5
+ # Title: setup_guide
6
+ # Author: Zaihua Ji, zji@ucar.edu
7
+ # Date: 2026-05-21
8
+ # Purpose: Shared setuid setup guide displayed when a package's setuid_* entry
9
+ # point is invoked directly (i.e. before pywrapper symlinks are
10
+ # configured). Each package's setuid entry point calls
11
+ # show_setup_guide(pkgname, appnames) with its own metadata.
12
+ #
13
+ # Github: https://github.com/NCAR/rda-python-setuid.git
14
+ #
15
+ ##################################################################################
16
+
17
+ import os
18
+ import sys
19
+
20
+
21
+ def show_setup_guide(obj, pkgname, appnames):
22
+ """Display the setuid setup guide if invoked directly, otherwise return.
23
+
24
+ When a package's setuid entry point (e.g. ``setuid_dsarch``) is invoked
25
+ directly before pywrapper symlinks are set up, ``obj.get_command()``
26
+ returns the literal ``setuid_<appname>`` (no prefix stripping, since
27
+ euid is the real user, not COMMONUSER). In that case this function reads
28
+ ``setuid_setup.usg`` bundled with rda_python_setuid, substitutes
29
+ ``{PKGNAME}`` and ``{APPNAMES}``, prints the guide, and exits.
30
+
31
+ When invoked via the pywrapper symlink (euid = COMMONUSER), the
32
+ ``setuid_`` prefix is stripped by ``get_command()``, the membership
33
+ check fails, and this function returns silently so the program runs
34
+ normally.
35
+
36
+ Args:
37
+ obj: An instance derived from PgLOG (e.g. DsArch, RdaCp); provides
38
+ ``get_command()`` with access to ``self.PGLOG['COMMONUSER']``.
39
+ pkgname: Distribution name (e.g. ``rda_python_dsarch``).
40
+ appnames: List of program names provided by the package that need
41
+ setuid (e.g. ``['dsarch']`` or ``['rdacp', 'rdakill', 'rdamod']``).
42
+ """
43
+ if obj.get_command(sys.argv[0]) not in ['setuid_' + a for a in appnames]:
44
+ return
45
+ usgfile = os.path.join(os.path.dirname(__file__), 'setuid_setup.usg')
46
+ with open(usgfile) as f:
47
+ text = f.read()
48
+ text = text.replace('{PKGNAME}', pkgname)
49
+ text = text.replace('{APPNAMES}', ' '.join(appnames))
50
+ print(text)
51
+ sys.exit(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,15 @@
1
+ rda_python_setuid/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
+ rda_python_setuid/cmwrapper.c,sha256=37pRJSOsC88r6muH4IQZhGfN6wcENUd0o65-5TPdY78,3536
3
+ rda_python_setuid/install.py,sha256=H2km3ei_BAC3eC4_6x399gQcCmt0ESfjrlozzEMY0GA,14872
4
+ rda_python_setuid/install.usg,sha256=-z15I_2ytKShXyMNoz4ei6XARV807R5SoZfamheZrHM,9198
5
+ rda_python_setuid/pgstart.py,sha256=tYCOTJedqE5Lc6hUD2tyQparsOK9VGJ2alHHLNWqF20,3968
6
+ rda_python_setuid/pywrapper.c,sha256=Dp9EOFU9IShduQ4WMeFRiiIJLQvgZHsiQlc89vJgtII,4190
7
+ rda_python_setuid/pywrapper.py,sha256=7RHpWeyHGLP_7Vx3KJ_yy-3qYObYg_ar6meuworA0MA,3023
8
+ rda_python_setuid/setuid_setup.usg,sha256=nJaS08GtlCQmXiy-udp6cxCFgZZJuBaZ36DicUn5Q7I,2915
9
+ rda_python_setuid/setup_guide.py,sha256=thdTlYJwhID63G7G9NWte0qeXwH6ytQPLFbjesWodNQ,2158
10
+ rda_python_setuid-3.0.1.dist-info/licenses/LICENSE,sha256=1dck4EAQwv8QweDWCXDx-4Or0S8YwiCstaso_H57Pno,1097
11
+ rda_python_setuid-3.0.1.dist-info/METADATA,sha256=QzPPHYBqLKoKi6YhtGiv8DHRNHA5q1_R06ihwMEQo84,10714
12
+ rda_python_setuid-3.0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
13
+ rda_python_setuid-3.0.1.dist-info/entry_points.txt,sha256=mWZUCa2KYzGsYVa33M7W03CY9SMsJYsywlIDF-4iMaQ,165
14
+ rda_python_setuid-3.0.1.dist-info/top_level.txt,sha256=ONMhKLagyTBktuz5dTyipSnRC0YhomTMs8eFRjM9kHQ,18
15
+ rda_python_setuid-3.0.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,147 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: rda_python_setuid
3
- Version: 1.0.9
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
- Two 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_zji` runs any command as specialist
31
- `zji` via `pgstart.py`, restricted to authorized users.
32
-
33
- Two Python entry points are packaged alongside the C wrapper:
34
-
35
- - **`pywrapper.py`** — the default fallback target executed when `pywrapper.c`
36
- cannot resolve a matching `setuid_<program>` entry point. Acquires the
37
- effective UID via `PgLOG.set_suid()`, prints the caller's real and effective
38
- user names, and shows the `pyproject.toml` snippet plus the
39
- `pywrapper-install -l <program>` command needed to wrap a new script.
40
- Diagnostic flags `-env`, `-inc`, and `-plg` dump the environment variables,
41
- `sys.path`, and `PGLOG` dictionary respectively — handy for verifying the
42
- setuid environment before wiring up a real program.
43
-
44
- - **`pgstart.py`** — the Mode 2 launcher invoked through a `pgstart_<USER>`
45
- copy of `pywrapper`. Reads the real/effective UIDs from `PGLOG`, then
46
- permits execution only if the real user matches the effective user or the
47
- shared GDEX common user (`PGLOG['GDEXUSER']`); unauthorized callers receive
48
- an informational message and exit. After authorization it parses leading
49
- flag tokens — `-bg` (background via `subprocess.Popen`), `-fg` (explicit
50
- foreground, default), `-cwd <dir>` (chdir before exec), and the same
51
- `-env`/`-inc`/`-plg` diagnostics as `pywrapper.py` — and then runs the
52
- remaining arguments as a command (`subprocess.run`/`Popen`) under the
53
- effective UID, logging a host/program/timestamp/user line to `pgstart.log`.
54
-
55
- ## Dependency requirement
56
-
57
- Any Python package whose programs are to be run via the setuid mechanism must declare
58
- `rda_python_setuid` as a dependency in its `pyproject.toml`:
59
-
60
- ```toml
61
- [project]
62
- dependencies = [
63
- "rda_python_setuid",
64
- ...
65
- ]
66
- ```
67
-
68
- It must also register each wrapped program's connector entry point with a `setuid_`
69
- prefix:
70
-
71
- ```toml
72
- [project.scripts]
73
- "setuid_dsarch" = "rda_python_dsarch.dsarch:main"
74
- ```
75
-
76
- `pip install` then places `setuid_dsarch` in the environment's `bin/` directory
77
- automatically. `pywrapper-install -l/--link` creates the symlink
78
- `dsarch -> pywrapper`; running `dsarch` goes through the setuid wrapper, which
79
- execs `setuid_dsarch` as CommonUser.
80
-
81
- ## Environment setup
82
-
83
- ### Option A — Python venv (DECS machines)
84
-
85
- ```bash
86
- python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
87
- source $ENVHOME/bin/activate
88
- pip install rda_python_setuid rda_python_dsarch ...
89
- ```
90
-
91
- ### Option B — Conda (DAV/Casper)
92
-
93
- ```bash
94
- conda create -n pg-gdex python=3.10
95
- conda activate pg-gdex
96
- pip install rda_python_setuid rda_python_dsarch ...
97
- ```
98
-
99
- The conda environment is typically at `/glade/work/gdexdata/conda-envs/pg-gdex`.
100
-
101
- ## Installation
102
-
103
- After setting up the environment and installing packages, run `pywrapper-install`
104
- with no arguments to display the full user guide:
105
-
106
- ```bash
107
- pywrapper-install
108
- ```
109
-
110
- ### Full setuid setup (requires sudo access to CommonUser)
111
-
112
- ```bash
113
- # 1. Install the target package (pulls in rda_python_setuid automatically):
114
- pip install rda_python_dsarch
115
-
116
- # 2. Compile pywrapper C binary (once per environment):
117
- pywrapper-install -c|--compile
118
-
119
- # 3. Wire up each program as a setuid entry:
120
- pywrapper-install -l|--link dsarch
121
-
122
- # 4. Optionally, allow a specialist to run commands as themselves:
123
- pywrapper-install -p|--pgstart -u|--user zji
124
- ```
125
-
126
- ### Simple install (no sudo required, runs as current user)
127
-
128
- Users who do not need the setuid mechanism can skip steps 2–4 and create a
129
- direct symlink from `dsarch` to `setuid_dsarch`:
130
-
131
- ```bash
132
- pip install rda_python_dsarch
133
- pywrapper-install -l|--link dsarch -s|--simple
134
- ```
135
-
136
- ## Runtime flow
137
-
138
- ```
139
- user runs: dsarch [args]
140
- | (symlink -> pywrapper, setuid bit -> EUID=gdexdata)
141
- pywrapper.c: execv(bin/setuid_dsarch, args)
142
- setuid_dsarch: calls dsarch:main() as gdexdata
143
- ```
144
-
145
- ## Github
146
-
147
- <https://github.com/NCAR/rda-python-setuid>
@@ -1,12 +0,0 @@
1
- rda_python_setuid/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
- rda_python_setuid/install.py,sha256=0tY0zo7WRyQMm0iUi1_XYyjR8SDVU17R729ehATZNKw,6071
3
- rda_python_setuid/install.usg,sha256=m0tgMS_Jh9-1wxCkbbgDNVJb3MGlMSp0Ecnwc9UeQcI,5582
4
- rda_python_setuid/pgstart.py,sha256=qM-ntQbYfEK1VYigXbNFBDOfW9nyaezjsjyomauMTbw,3945
5
- rda_python_setuid/pywrapper.c,sha256=Dp9EOFU9IShduQ4WMeFRiiIJLQvgZHsiQlc89vJgtII,4190
6
- rda_python_setuid/pywrapper.py,sha256=pyQZfITTpxQ_lVkWc_0ZJRF_sZnq8NQdOvR5lObu6mw,2898
7
- rda_python_setuid-1.0.9.dist-info/licenses/LICENSE,sha256=1dck4EAQwv8QweDWCXDx-4Or0S8YwiCstaso_H57Pno,1097
8
- rda_python_setuid-1.0.9.dist-info/METADATA,sha256=jFM7488gCNataDKePCLkILCVPD1xZ0i_2iAOSx46piA,5027
9
- rda_python_setuid-1.0.9.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
- rda_python_setuid-1.0.9.dist-info/entry_points.txt,sha256=mWZUCa2KYzGsYVa33M7W03CY9SMsJYsywlIDF-4iMaQ,165
11
- rda_python_setuid-1.0.9.dist-info/top_level.txt,sha256=ONMhKLagyTBktuz5dTyipSnRC0YhomTMs8eFRjM9kHQ,18
12
- rda_python_setuid-1.0.9.dist-info/RECORD,,