rda-python-setuid 1.0.9__py3-none-any.whl → 3.0.0__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.
@@ -15,17 +15,23 @@
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
+ #
29
35
  # Convention for wrapped programs:
30
36
  # The target package must register its connector entry point with a setuid_ prefix:
31
37
  # [project.scripts]
@@ -77,7 +83,7 @@ def main():
77
83
  help="Path to the venv root directory containing bin/ (default: parent of the current Python executable's bin/ dir)"
78
84
  )
79
85
  parser.add_argument(
80
- '-u', '--user', default=None,
86
+ '-n', '--username', default=None,
81
87
  help="User name to own the setuid binary (default: current login user for -p/--pgstart, gdexdata otherwise)"
82
88
  )
83
89
  parser.add_argument(
@@ -95,57 +101,105 @@ def main():
95
101
  )
96
102
  group.add_argument(
97
103
  '-l', '--link', metavar='PROGRAM',
98
- help="Create symlink PROGRAM -> pywrapper for running a fixed program as CommonUser (Mode 1)"
104
+ 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
+ )
106
+ group.add_argument(
107
+ '-u', '--update', action='store_true',
108
+ help="Update an existing installation: recompile pywrapper and reinstall all pgstart_USER setuid binaries"
99
109
  )
100
110
  args = parser.parse_args()
101
111
 
102
- if not (args.compile or args.pgstart or args.link):
112
+ if not (args.compile or args.pgstart or args.link or args.update):
103
113
  show_usage()
104
114
 
105
- if args.user is None and not args.simple:
115
+ if args.username is None and not args.simple:
106
116
  import pwd
107
117
  if args.pgstart:
108
- args.user = pwd.getpwuid(os.getuid()).pw_name
118
+ args.username = pwd.getpwuid(os.getuid()).pw_name
109
119
  else:
110
- args.user = 'gdexdata'
120
+ args.username = 'gdexdata'
111
121
 
112
122
  bindir = os.path.join(args.envhome, 'bin') if args.envhome else get_bindir()
113
123
  pywrapper = os.path.join(bindir, 'pywrapper')
114
124
 
115
125
  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))
126
+ # For appname -> pywrapper links, run `ln -s` via pgstart_<commonuser> so
127
+ # the resulting symlink is owned by the common user (pywrapper owner).
128
+ pgstart_common = None
129
+ if not args.simple and os.path.exists(pywrapper):
130
+ import pwd
131
+ common_user = pwd.getpwuid(os.stat(pywrapper).st_uid).pw_name
132
+ pgstart_common = os.path.join(bindir, 'pgstart_' + common_user)
133
+ if not os.path.exists(pgstart_common):
134
+ print("Error: {} not found. Run pywrapper-install --pgstart --username {} first.".format(pgstart_common, common_user))
135
+ sys.exit(1)
136
+
137
+ if args.link.lower() == 'all':
138
+ # Discover all setuid_* entries in bindir and link any that are missing
139
+ appnames = sorted(
140
+ f[len('setuid_'):] for f in os.listdir(bindir) if f.startswith('setuid_')
141
+ )
142
+ if not appnames:
143
+ print("No setuid_* entries found in {}".format(bindir))
144
+ for appname in appnames:
145
+ target = os.path.join(bindir, appname)
146
+ script = os.path.join(bindir, 'setuid_' + appname)
147
+ if args.simple:
148
+ if os.path.lexists(target):
149
+ print("Already exists: {}".format(target))
150
+ else:
151
+ os.symlink(script, target)
152
+ print("Created: {} -> setuid_{}".format(target, appname))
153
+ else:
154
+ if os.path.lexists(target):
155
+ print("Already exists: {}".format(target))
156
+ else:
157
+ run([pgstart_common, 'ln', '-s', pywrapper, target])
158
+ print("Created: {} -> pywrapper".format(target))
128
159
  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))
160
+ target = os.path.join(bindir, args.link)
161
+ script = os.path.join(bindir, 'setuid_{}'.format(args.link))
162
+ if not os.path.exists(script):
163
+ print("Error: {} not found. Install the package that provides it first.".format(script))
164
+ sys.exit(1)
165
+ if args.simple:
166
+ # Simple install: symlink PROGRAM -> setuid_PROGRAM, no setuid, runs as current user.
167
+ if os.path.lexists(target):
168
+ print("Already exists: {}".format(target))
169
+ else:
170
+ os.symlink(script, target)
171
+ print("Created: {} -> setuid_{}".format(target, args.link))
133
172
  else:
134
- os.symlink(pywrapper, target)
135
- print("Created: {} -> pywrapper".format(target))
173
+ # Mode 1: symlink PROGRAM -> pywrapper. setuid_PROGRAM is left with its
174
+ # default ownership/permissions so it can be loaded and executed normally.
175
+ if os.path.lexists(target):
176
+ print("Already exists: {}".format(target))
177
+ else:
178
+ run([pgstart_common, 'ln', '-s', pywrapper, target])
179
+ print("Created: {} -> pywrapper".format(target))
136
180
 
137
181
  elif args.pgstart:
138
- # Mode 2: copy pywrapper to pgstart_USER with setuid owned by USER
182
+ # Mode 2: create pgstart_USER with setuid owned by USER.
183
+ # When USER already owns pywrapper (i.e. the common user), pywrapper is
184
+ # already setuid owned by USER, so a symlink is sufficient; otherwise
185
+ # copy pywrapper and chmod 4750 as USER.
139
186
  if not os.path.exists(pywrapper):
140
- print("Error: {} not found. Run pywrapper-install --user COMMONUSER first.".format(pywrapper))
187
+ print("Error: {} not found. Run pywrapper-install --compile --username COMMONUSER first.".format(pywrapper))
141
188
  sys.exit(1)
142
- target = os.path.join(bindir, 'pgstart_{}'.format(args.user))
189
+ target = os.path.join(bindir, 'pgstart_{}'.format(args.username))
143
190
  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))
191
+ pywrapper_owner = pwd.getpwuid(os.stat(pywrapper).st_uid).pw_name
192
+ if args.username == pywrapper_owner:
193
+ if os.path.lexists(target):
194
+ os.remove(target)
195
+ os.symlink(pywrapper, target)
196
+ print("Linked: {} -> pywrapper (setuid, owned by {})".format(target, args.username))
197
+ else:
198
+ curuser = pwd.getpwuid(os.getuid()).pw_name
199
+ sudo_prefix = [] if curuser == args.username else ['sudo', '-u', args.username]
200
+ run(sudo_prefix + ['cp', pywrapper, target])
201
+ run(sudo_prefix + ['chmod', '4750', target])
202
+ print("Installed: {} (setuid, owned by {})".format(target, args.username))
149
203
 
150
204
  elif args.compile:
151
205
  # Compile pywrapper.c and install pywrapper with setuid
@@ -153,9 +207,80 @@ def main():
153
207
  src_dest = os.path.join(bindir, 'pywrapper.c')
154
208
  shutil.copy(src, src_dest)
155
209
  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))
210
+ run(['sudo', '-u', args.username, 'gcc', '-o', pywrapper, src_dest])
211
+ run(['sudo', '-u', args.username, 'chmod', '4750', pywrapper])
212
+ print("Installed: {} (setuid, owned by {})".format(pywrapper, args.username))
213
+
214
+ elif args.update:
215
+ # Update an existing installation: recompile pywrapper and reinstall all setuid binaries
216
+ pgstart_files = sorted(f for f in os.listdir(bindir) if f.startswith('pgstart_'))
217
+ if not pgstart_files:
218
+ print("Error: No pgstart_* binaries found in {}".format(bindir))
219
+ sys.exit(1)
220
+ if not os.path.exists(pywrapper):
221
+ print("Error: {} not found.".format(pywrapper))
222
+ sys.exit(1)
223
+
224
+ gdexuser = args.username
225
+ pgstart_gdexdata = os.path.join(bindir, 'pgstart_' + gdexuser)
226
+ if not os.path.exists(pgstart_gdexdata):
227
+ print("Error: {} not found.".format(pgstart_gdexdata))
228
+ sys.exit(1)
229
+
230
+ update_tmp = os.path.join(bindir, 'update_tmp')
231
+ os.makedirs(update_tmp, exist_ok=True)
232
+ print("Created: {}".format(update_tmp))
233
+
234
+ # Symlink pgstart.py into update_tmp so pywrapper instances running from
235
+ # update_tmp can find it via the fpath/pgstart.py fallback lookup.
236
+ bindir_pgstart_py = os.path.join(bindir, 'pgstart.py')
237
+ update_pgstart_py = os.path.join(update_tmp, 'pgstart.py')
238
+ if not os.path.lexists(update_pgstart_py):
239
+ os.symlink(bindir_pgstart_py, update_pgstart_py)
240
+ print("Linked: {} -> {}".format(update_pgstart_py, bindir_pgstart_py))
241
+
242
+ # Copy each non-gdex pgstart_USERNAME into update_tmp using itself, then chmod 4750
243
+ for fname in pgstart_files:
244
+ username = fname[len('pgstart_'):]
245
+ if username == gdexuser:
246
+ continue
247
+ src_pgstart = os.path.join(bindir, fname)
248
+ dst_pgstart = os.path.join(update_tmp, fname)
249
+ run([src_pgstart, 'cp', src_pgstart, update_tmp + '/'])
250
+ run([src_pgstart, 'chmod', '4750', dst_pgstart])
251
+
252
+ # Copy pywrapper into update_tmp as gdexdata, chmod, then hardlink as pgstart_gdexdata
253
+ update_pywrapper = os.path.join(update_tmp, 'pywrapper')
254
+ update_pgstart_gdexdata = os.path.join(update_tmp, 'pgstart_' + gdexuser)
255
+ run([pgstart_gdexdata, 'cp', pywrapper, update_tmp + '/'])
256
+ run([pgstart_gdexdata, 'chmod', '4750', update_pywrapper])
257
+ run([pgstart_gdexdata, 'ln', update_pywrapper, update_pgstart_gdexdata])
258
+
259
+ # Compile new pywrapper using update_tmp/pgstart_gdexdata
260
+ src = get_c_source()
261
+ src_dest = os.path.join(bindir, 'pywrapper.c')
262
+ shutil.copy(src, src_dest)
263
+ print("Copied: {}".format(src_dest))
264
+ run([update_pgstart_gdexdata, 'gcc', '-o', pywrapper, src_dest])
265
+ run([update_pgstart_gdexdata, 'chmod', '4750', pywrapper])
266
+ print("Compiled: {} (setuid, owned by {})".format(pywrapper, gdexuser))
267
+
268
+ # Recreate each pgstart_* in bindir using the corresponding update_tmp/pgstart_*
269
+ for fname in sorted(f for f in os.listdir(update_tmp) if f.startswith('pgstart_')):
270
+ username = fname[len('pgstart_'):]
271
+ update_pgstart = os.path.join(update_tmp, fname)
272
+ target = os.path.join(bindir, fname)
273
+ if username == gdexuser:
274
+ run([update_pgstart_gdexdata, 'ln', '-sf', pywrapper, target])
275
+ print("Linked: {} -> pywrapper (setuid, owned by {})".format(target, gdexuser))
276
+ else:
277
+ run([update_pgstart, 'cp', pywrapper, target])
278
+ run([update_pgstart, 'chmod', '4750', target])
279
+ print("Updated: {} (setuid, owned by {})".format(target, username))
280
+
281
+ # Clean up the temporary working directory
282
+ shutil.rmtree(update_tmp)
283
+ print("Removed: {}".format(update_tmp))
159
284
 
160
285
 
161
286
  if __name__ == '__main__': main()
@@ -2,16 +2,17 @@
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
+ | -u|--update )
9
10
 
10
11
  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.
12
+ one of -c/--compile, -p/--pgstart, -l/--link, or -u/--update must be given
13
+ to perform an action.
13
14
 
14
- - Option -u or --user USER
15
+ - Option -n or --username USERNAME
15
16
  The user name to own the setuid binary. For Mode 1 (CommonUser program),
16
17
  this is the common user (e.g. gdexdata). For Mode 2 (pgstart), this is
17
18
  the specialist user (e.g. zji). Defaults to 'gdexdata' unless -p/--pgstart
@@ -31,20 +32,34 @@
31
32
 
32
33
  - Option -p or --pgstart
33
34
  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
35
+ chmod 4750). Allows USER (any login user in the same group as the
36
+ common user PGLOG['COMMONUSER']) to run arbitrary commands as themselves
37
+ via the setuid wrapper. Two paths for setting up pgstart_USER:
38
+ (a) if PGLOG['ADMINUSER'] (default zji) can 'sudo -u USER', the admin
39
+ runs 'pywrapper-install -p -n USER' on behalf of USER; or
40
+ (b) USER runs the same command themselves (no sudo required).
41
+ Cannot be combined with -c/--compile or -l/--link.
42
+
43
+ - Option -l or --link PROGRAM|all
38
44
  Mode 1: create a symlink PROGRAM -> pywrapper in the bin/ directory so
39
45
  that running PROGRAM invokes the setuid wrapper, which then execs
40
46
  setuid_PROGRAM as CommonUser. setuid_PROGRAM keeps its default
41
47
  ownership and permissions. Cannot be combined with -c/--compile or
42
48
  -p/--pgstart.
49
+ Use 'all' instead of a program name to scan bin/ for every setuid_*
50
+ entry and add any missing PROGRAM -> pywrapper symlinks in one pass.
51
+
52
+ - Option -u or --update
53
+ Update an existing installation. Discovers all pgstart_* and pywrapper
54
+ binaries already present in bin/, saves them to a temporary update_tmp/
55
+ subdirectory, recompiles pywrapper using update_tmp/pgstart_COMMONUSER, and
56
+ reinstalls every pgstart_* binary from update_tmp. Use -n/--username to
57
+ specify the gdex common user (default: gdexdata).
43
58
 
44
59
  - Option -s or --simple (use with -l/--link)
45
60
  Simple install: create a symlink PROGRAM -> setuid_PROGRAM directly,
46
61
  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.
62
+ user with no privilege change. -n/--username is not required with this option.
48
63
  Useful for users who do not need or cannot set up the setuid wrapper.
49
64
 
50
65
  Convention for wrapped programs:
@@ -111,10 +126,14 @@
111
126
  pywrapper-install -c
112
127
 
113
128
  3. Wire up each program as a setuid entry:
114
- pywrapper-install -l dsarch
129
+ pywrapper-install -l dsarch # one program
130
+ pywrapper-install -l all # or all setuid_* entries at once
115
131
 
116
- 4. Optionally, allow a specialist to run commands as themselves:
117
- pywrapper-install -p
132
+ 4. Optionally, install a pgstart_<loginname> binary so <loginname> (any
133
+ user in the same group as PGLOG['COMMONUSER']) can run commands as
134
+ themselves. Either PGLOG['ADMINUSER'] (default zji, if it has
135
+ 'sudo -u <loginname>'), or <loginname> directly, runs:
136
+ pywrapper-install -p -n <loginname>
118
137
 
119
138
  Option B - Simple install (no sudo required, runs as current user):
120
139
 
@@ -122,7 +141,8 @@
122
141
  pip install rda_python_dsarch
123
142
 
124
143
  2. Create a direct symlink to the connector script:
125
- pywrapper-install -l dsarch -s
144
+ pywrapper-install -l dsarch -s # one program
145
+ pywrapper-install -l all -s # or all setuid_* entries at once
126
146
 
127
147
  Examples:
128
148
 
@@ -135,8 +155,18 @@
135
155
  3. Wire up dsarch to run as gdexdata via pywrapper:
136
156
  pywrapper-install -l dsarch
137
157
 
138
- 4. Allow specialist zji to run commands as themselves via pgstart:
139
- pywrapper-install -p -u zji
158
+ 4. Wire up all setuid_* entries at once:
159
+ pywrapper-install -l all
140
160
 
141
- 5. Simple install of dsarch for a user who does not need setuid:
161
+ 5. Install pgstart_zji so login user zji can run commands as themselves
162
+ via pgstart (run by ADMINUSER with 'sudo -u zji' available, or by zji):
163
+ pywrapper-install -p -n zji
164
+
165
+ 6. Update an existing installation (recompile and reinstall all setuid binaries):
166
+ pywrapper-install -u
167
+
168
+ 7. Simple install of dsarch for a user who does not need setuid:
142
169
  pywrapper-install -l dsarch -s
170
+
171
+ 8. Simple install of all setuid_* entries at once:
172
+ pywrapper-install -l all -s
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_setuid
3
- Version: 1.0.9
3
+ Version: 3.0.0
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
@@ -27,8 +27,10 @@ Two modes are supported:
27
27
 
28
28
  - **Mode 1 (CommonUser program):** a symlink `dsarch -> pywrapper` runs `setuid_dsarch`
29
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.
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).
32
34
 
33
35
  Two Python entry points are packaged alongside the C wrapper:
34
36
 
@@ -41,10 +43,12 @@ Two Python entry points are packaged alongside the C wrapper:
41
43
  `sys.path`, and `PGLOG` dictionary respectively — handy for verifying the
42
44
  setuid environment before wiring up a real program.
43
45
 
44
- - **`pgstart.py`** — the Mode 2 launcher invoked through a `pgstart_<USER>`
46
+ - **`pgstart.py`** — the Mode 2 launcher invoked through a `pgstart_<loginname>`
45
47
  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
+ permits execution only if the real user is in
49
+ `[PGLOG['ADMINUSER'], euser, PGLOG['COMMONUSER']]`
50
+ (i.e. the admin specialist `PGLOG['ADMINUSER']` — default `zji` — the
51
+ effective user themselves, or the shared common user); unauthorized callers receive
48
52
  an informational message and exit. After authorization it parses leading
49
53
  flag tokens — `-bg` (background via `subprocess.Popen`), `-fg` (explicit
50
54
  foreground, default), `-cwd <dir>` (chdir before exec), and the same
@@ -78,30 +82,84 @@ automatically. `pywrapper-install -l/--link` creates the symlink
78
82
  `dsarch -> pywrapper`; running `dsarch` goes through the setuid wrapper, which
79
83
  execs `setuid_dsarch` as CommonUser.
80
84
 
85
+ The `main()` of each wrapped program (e.g. `rda_python_dsarch/dsarch.py`) must
86
+ also call `show_setup_guide()` at the top of `main()`, passing an instance of
87
+ the program's class along with the package name and list of setuid program
88
+ names:
89
+
90
+ ```python
91
+ def main():
92
+ from rda_python_setuid.setup_guide import show_setup_guide
93
+ object = DsArch()
94
+ show_setup_guide(object, 'rda_python_dsarch', ['dsarch'])
95
+ ...
96
+ ```
97
+
98
+ When `setuid_dsarch` is invoked directly (before pywrapper symlinks are set
99
+ up, so euid ≠ CommonUser), `show_setup_guide()` prints the shared setuid setup
100
+ guide and exits. When invoked via the `dsarch -> pywrapper` symlink (euid =
101
+ CommonUser), `get_command()` strips the `setuid_` prefix, the check inside
102
+ `show_setup_guide()` fails, and the program runs normally.
103
+
81
104
  ## Environment setup
82
105
 
106
+ Create a Python environment first; package installs in the next section run
107
+ inside whichever environment you activate here.
108
+
83
109
  ### Option A — Python venv (DECS machines)
84
110
 
85
111
  ```bash
86
112
  python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
87
113
  source $ENVHOME/bin/activate
88
- pip install rda_python_setuid rda_python_dsarch ...
89
114
  ```
90
115
 
91
116
  ### Option B — Conda (DAV/Casper)
92
117
 
93
118
  ```bash
94
- conda create -n pg-gdex python=3.10
95
- conda activate pg-gdex
96
- pip install rda_python_setuid rda_python_dsarch ...
119
+ conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
120
+ conda activate $ENVHOME
97
121
  ```
98
122
 
99
- The conda environment is typically at `/glade/work/gdexdata/conda-envs/pg-gdex`.
123
+ ## Installing rda-python-setuid
100
124
 
101
- ## Installation
125
+ Pick whichever install mode fits your workflow. All four pull in the
126
+ transitive dependency (`rda_python_common`) automatically. Once installed,
127
+ the `pywrapper-install` CLI is available for the setuid wiring steps below.
102
128
 
103
- After setting up the environment and installing packages, run `pywrapper-install`
104
- with no arguments to display the full user guide:
129
+ For local development, clone this repo alongside your project and install it
130
+ in editable mode so that changes are picked up without re-installing:
131
+
132
+ ```bash
133
+ git clone https://github.com/NCAR/rda-python-setuid.git
134
+ cd rda-python-setuid
135
+ pip install -e .
136
+ ```
137
+
138
+ To test a specific branch (e.g. an in-progress feature or fix branch), pass
139
+ `-b/--branch` to `git clone`:
140
+
141
+ ```bash
142
+ git clone -b <branch-name> https://github.com/NCAR/rda-python-setuid.git
143
+ cd rda-python-setuid
144
+ pip install -e .
145
+ ```
146
+
147
+ For a regular (non-editable) install from a checkout:
148
+
149
+ ```bash
150
+ pip install /path/to/rda-python-setuid
151
+ ```
152
+
153
+ For a production install on a system that uses the published distribution:
154
+
155
+ ```bash
156
+ pip install rda_python_setuid
157
+ ```
158
+
159
+ ## Setuid wrapper setup
160
+
161
+ With `rda_python_setuid` installed in the active environment, run
162
+ `pywrapper-install` with no arguments to display the full user guide:
105
163
 
106
164
  ```bash
107
165
  pywrapper-install
@@ -116,11 +174,32 @@ pip install rda_python_dsarch
116
174
  # 2. Compile pywrapper C binary (once per environment):
117
175
  pywrapper-install -c|--compile
118
176
 
119
- # 3. Wire up each program as a setuid entry:
177
+ # 3. Wire up each program as a setuid entry (specify name or use 'all'):
120
178
  pywrapper-install -l|--link dsarch
179
+ pywrapper-install -l|--link all # auto-link every setuid_* entry not yet linked
180
+
181
+ # 4. Optionally, install a pgstart_<loginname> binary so <loginname> (any user
182
+ # in the same group as PGLOG['COMMONUSER']) can run commands as themselves
183
+ # via the setuid wrapper. Same command in both cases — only the invoker
184
+ # differs:
185
+ #
186
+ # 4a. If PGLOG['ADMINUSER'] (default zji) can `sudo -u <loginname>`, the
187
+ # admin sets it up on the user's behalf:
188
+ pywrapper-install -p|--pgstart -n|--username <loginname>
189
+ #
190
+ # 4b. Otherwise <loginname> runs the same command themselves (no sudo
191
+ # from ADMINUSER required, since they already are <loginname>):
192
+ pywrapper-install -p|--pgstart -n|--username <loginname>
193
+ ```
121
194
 
122
- # 4. Optionally, allow a specialist to run commands as themselves:
123
- pywrapper-install -p|--pgstart -u|--user zji
195
+ ### Update an existing installation (no sudo required)
196
+
197
+ When the package is upgraded and a new `pywrapper.c` is bundled, use `-u/--update`
198
+ to recompile and reinstall all setuid binaries without needing `sudo`. The existing
199
+ `pgstart_*` binaries in `bin/` are used to perform the privileged operations:
200
+
201
+ ```bash
202
+ pywrapper-install -u|--update [-n|--username gdexdata] [-e|--envhome $ENVHOME]
124
203
  ```
125
204
 
126
205
  ### Simple install (no sudo required, runs as current user)
@@ -131,6 +210,7 @@ direct symlink from `dsarch` to `setuid_dsarch`:
131
210
  ```bash
132
211
  pip install rda_python_dsarch
133
212
  pywrapper-install -l|--link dsarch -s|--simple
213
+ pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
134
214
  ```
135
215
 
136
216
  ## Runtime flow
@@ -0,0 +1,14 @@
1
+ rda_python_setuid/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
+ rda_python_setuid/install.py,sha256=IguehVEMgY08-mNZcDPL4sMX5_XF5sb71qwkzE-wTqU,12422
3
+ rda_python_setuid/install.usg,sha256=CEeRuUO4_3dzwlIk0uEbDl-6HtYNGm16pltBvNfJqkw,7397
4
+ rda_python_setuid/pgstart.py,sha256=tYCOTJedqE5Lc6hUD2tyQparsOK9VGJ2alHHLNWqF20,3968
5
+ rda_python_setuid/pywrapper.c,sha256=Dp9EOFU9IShduQ4WMeFRiiIJLQvgZHsiQlc89vJgtII,4190
6
+ rda_python_setuid/pywrapper.py,sha256=7RHpWeyHGLP_7Vx3KJ_yy-3qYObYg_ar6meuworA0MA,3023
7
+ rda_python_setuid/setuid_setup.usg,sha256=nJaS08GtlCQmXiy-udp6cxCFgZZJuBaZ36DicUn5Q7I,2915
8
+ rda_python_setuid/setup_guide.py,sha256=thdTlYJwhID63G7G9NWte0qeXwH6ytQPLFbjesWodNQ,2158
9
+ rda_python_setuid-3.0.0.dist-info/licenses/LICENSE,sha256=1dck4EAQwv8QweDWCXDx-4Or0S8YwiCstaso_H57Pno,1097
10
+ rda_python_setuid-3.0.0.dist-info/METADATA,sha256=JTvNrLDE7D3GZV1-Rr2ElyksAB8Bv0FmSVgsazKwnR0,8184
11
+ rda_python_setuid-3.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ rda_python_setuid-3.0.0.dist-info/entry_points.txt,sha256=mWZUCa2KYzGsYVa33M7W03CY9SMsJYsywlIDF-4iMaQ,165
13
+ rda_python_setuid-3.0.0.dist-info/top_level.txt,sha256=ONMhKLagyTBktuz5dTyipSnRC0YhomTMs8eFRjM9kHQ,18
14
+ rda_python_setuid-3.0.0.dist-info/RECORD,,
@@ -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,,