rda-python-setuid 1.0.9__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rda_python_setuid-1.0.9/src/rda_python_setuid.egg-info → rda_python_setuid-3.0.0}/PKG-INFO +97 -17
- rda_python_setuid-3.0.0/README.md +211 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/pyproject.toml +2 -2
- rda_python_setuid-3.0.0/src/rda_python_setuid/install.py +286 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid/install.usg +47 -17
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid/pgstart.py +3 -3
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid/pywrapper.py +2 -0
- rda_python_setuid-3.0.0/src/rda_python_setuid/setuid_setup.usg +75 -0
- rda_python_setuid-3.0.0/src/rda_python_setuid/setup_guide.py +51 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0/src/rda_python_setuid.egg-info}/PKG-INFO +97 -17
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid.egg-info/SOURCES.txt +2 -0
- rda_python_setuid-1.0.9/README.md +0 -131
- rda_python_setuid-1.0.9/src/rda_python_setuid/install.py +0 -161
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/LICENSE +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/MANIFEST.in +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/setup.cfg +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid/__init__.py +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid/pywrapper.c +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid.egg-info/dependency_links.txt +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid.egg-info/entry_points.txt +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid.egg-info/requires.txt +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/src/rda_python_setuid.egg-info/top_level.txt +0 -0
- {rda_python_setuid-1.0.9 → rda_python_setuid-3.0.0}/tests/test_setuid.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rda_python_setuid
|
|
3
|
-
Version:
|
|
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`
|
|
31
|
-
|
|
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_<
|
|
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
|
|
47
|
-
|
|
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
|
|
95
|
-
conda activate
|
|
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
|
-
|
|
123
|
+
## Installing rda-python-setuid
|
|
100
124
|
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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,211 @@
|
|
|
1
|
+
RDA Python package, including a C code wrapper, to execute commandline applications
|
|
2
|
+
via setuid for effective and common user names.
|
|
3
|
+
|
|
4
|
+
## Overview
|
|
5
|
+
|
|
6
|
+
`rda_python_setuid` provides a C binary (`pywrapper`) that acquires a setuid effective
|
|
7
|
+
user, then `execv`s a Python entry point script. This allows Python programs to run
|
|
8
|
+
as a designated common user (e.g. `gdexdata`) without requiring `sudo` access.
|
|
9
|
+
|
|
10
|
+
Two modes are supported:
|
|
11
|
+
|
|
12
|
+
- **Mode 1 (CommonUser program):** a symlink `dsarch -> pywrapper` runs `setuid_dsarch`
|
|
13
|
+
as the common user.
|
|
14
|
+
- **Mode 2 (pgstart specialist):** a copy `pgstart_<loginname>` (e.g. `pgstart_zji`)
|
|
15
|
+
runs any command as `<loginname>` via `pgstart.py`. `<loginname>` can be any
|
|
16
|
+
user that belongs to the same group as `PGLOG['COMMONUSER']`. Execution is
|
|
17
|
+
restricted to authorized callers (see `pgstart.py` below).
|
|
18
|
+
|
|
19
|
+
Two Python entry points are packaged alongside the C wrapper:
|
|
20
|
+
|
|
21
|
+
- **`pywrapper.py`** — the default fallback target executed when `pywrapper.c`
|
|
22
|
+
cannot resolve a matching `setuid_<program>` entry point. Acquires the
|
|
23
|
+
effective UID via `PgLOG.set_suid()`, prints the caller's real and effective
|
|
24
|
+
user names, and shows the `pyproject.toml` snippet plus the
|
|
25
|
+
`pywrapper-install -l <program>` command needed to wrap a new script.
|
|
26
|
+
Diagnostic flags `-env`, `-inc`, and `-plg` dump the environment variables,
|
|
27
|
+
`sys.path`, and `PGLOG` dictionary respectively — handy for verifying the
|
|
28
|
+
setuid environment before wiring up a real program.
|
|
29
|
+
|
|
30
|
+
- **`pgstart.py`** — the Mode 2 launcher invoked through a `pgstart_<loginname>`
|
|
31
|
+
copy of `pywrapper`. Reads the real/effective UIDs from `PGLOG`, then
|
|
32
|
+
permits execution only if the real user is in
|
|
33
|
+
`[PGLOG['ADMINUSER'], euser, PGLOG['COMMONUSER']]`
|
|
34
|
+
(i.e. the admin specialist `PGLOG['ADMINUSER']` — default `zji` — the
|
|
35
|
+
effective user themselves, or the shared common user); unauthorized callers receive
|
|
36
|
+
an informational message and exit. After authorization it parses leading
|
|
37
|
+
flag tokens — `-bg` (background via `subprocess.Popen`), `-fg` (explicit
|
|
38
|
+
foreground, default), `-cwd <dir>` (chdir before exec), and the same
|
|
39
|
+
`-env`/`-inc`/`-plg` diagnostics as `pywrapper.py` — and then runs the
|
|
40
|
+
remaining arguments as a command (`subprocess.run`/`Popen`) under the
|
|
41
|
+
effective UID, logging a host/program/timestamp/user line to `pgstart.log`.
|
|
42
|
+
|
|
43
|
+
## Dependency requirement
|
|
44
|
+
|
|
45
|
+
Any Python package whose programs are to be run via the setuid mechanism must declare
|
|
46
|
+
`rda_python_setuid` as a dependency in its `pyproject.toml`:
|
|
47
|
+
|
|
48
|
+
```toml
|
|
49
|
+
[project]
|
|
50
|
+
dependencies = [
|
|
51
|
+
"rda_python_setuid",
|
|
52
|
+
...
|
|
53
|
+
]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
It must also register each wrapped program's connector entry point with a `setuid_`
|
|
57
|
+
prefix:
|
|
58
|
+
|
|
59
|
+
```toml
|
|
60
|
+
[project.scripts]
|
|
61
|
+
"setuid_dsarch" = "rda_python_dsarch.dsarch:main"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`pip install` then places `setuid_dsarch` in the environment's `bin/` directory
|
|
65
|
+
automatically. `pywrapper-install -l/--link` creates the symlink
|
|
66
|
+
`dsarch -> pywrapper`; running `dsarch` goes through the setuid wrapper, which
|
|
67
|
+
execs `setuid_dsarch` as CommonUser.
|
|
68
|
+
|
|
69
|
+
The `main()` of each wrapped program (e.g. `rda_python_dsarch/dsarch.py`) must
|
|
70
|
+
also call `show_setup_guide()` at the top of `main()`, passing an instance of
|
|
71
|
+
the program's class along with the package name and list of setuid program
|
|
72
|
+
names:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
def main():
|
|
76
|
+
from rda_python_setuid.setup_guide import show_setup_guide
|
|
77
|
+
object = DsArch()
|
|
78
|
+
show_setup_guide(object, 'rda_python_dsarch', ['dsarch'])
|
|
79
|
+
...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
When `setuid_dsarch` is invoked directly (before pywrapper symlinks are set
|
|
83
|
+
up, so euid ≠ CommonUser), `show_setup_guide()` prints the shared setuid setup
|
|
84
|
+
guide and exits. When invoked via the `dsarch -> pywrapper` symlink (euid =
|
|
85
|
+
CommonUser), `get_command()` strips the `setuid_` prefix, the check inside
|
|
86
|
+
`show_setup_guide()` fails, and the program runs normally.
|
|
87
|
+
|
|
88
|
+
## Environment setup
|
|
89
|
+
|
|
90
|
+
Create a Python environment first; package installs in the next section run
|
|
91
|
+
inside whichever environment you activate here.
|
|
92
|
+
|
|
93
|
+
### Option A — Python venv (DECS machines)
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
|
|
97
|
+
source $ENVHOME/bin/activate
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Option B — Conda (DAV/Casper)
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
|
|
104
|
+
conda activate $ENVHOME
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Installing rda-python-setuid
|
|
108
|
+
|
|
109
|
+
Pick whichever install mode fits your workflow. All four pull in the
|
|
110
|
+
transitive dependency (`rda_python_common`) automatically. Once installed,
|
|
111
|
+
the `pywrapper-install` CLI is available for the setuid wiring steps below.
|
|
112
|
+
|
|
113
|
+
For local development, clone this repo alongside your project and install it
|
|
114
|
+
in editable mode so that changes are picked up without re-installing:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
git clone https://github.com/NCAR/rda-python-setuid.git
|
|
118
|
+
cd rda-python-setuid
|
|
119
|
+
pip install -e .
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
To test a specific branch (e.g. an in-progress feature or fix branch), pass
|
|
123
|
+
`-b/--branch` to `git clone`:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
git clone -b <branch-name> https://github.com/NCAR/rda-python-setuid.git
|
|
127
|
+
cd rda-python-setuid
|
|
128
|
+
pip install -e .
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For a regular (non-editable) install from a checkout:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pip install /path/to/rda-python-setuid
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
For a production install on a system that uses the published distribution:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
pip install rda_python_setuid
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Setuid wrapper setup
|
|
144
|
+
|
|
145
|
+
With `rda_python_setuid` installed in the active environment, run
|
|
146
|
+
`pywrapper-install` with no arguments to display the full user guide:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pywrapper-install
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Full setuid setup (requires sudo access to CommonUser)
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# 1. Install the target package (pulls in rda_python_setuid automatically):
|
|
156
|
+
pip install rda_python_dsarch
|
|
157
|
+
|
|
158
|
+
# 2. Compile pywrapper C binary (once per environment):
|
|
159
|
+
pywrapper-install -c|--compile
|
|
160
|
+
|
|
161
|
+
# 3. Wire up each program as a setuid entry (specify name or use 'all'):
|
|
162
|
+
pywrapper-install -l|--link dsarch
|
|
163
|
+
pywrapper-install -l|--link all # auto-link every setuid_* entry not yet linked
|
|
164
|
+
|
|
165
|
+
# 4. Optionally, install a pgstart_<loginname> binary so <loginname> (any user
|
|
166
|
+
# in the same group as PGLOG['COMMONUSER']) can run commands as themselves
|
|
167
|
+
# via the setuid wrapper. Same command in both cases — only the invoker
|
|
168
|
+
# differs:
|
|
169
|
+
#
|
|
170
|
+
# 4a. If PGLOG['ADMINUSER'] (default zji) can `sudo -u <loginname>`, the
|
|
171
|
+
# admin sets it up on the user's behalf:
|
|
172
|
+
pywrapper-install -p|--pgstart -n|--username <loginname>
|
|
173
|
+
#
|
|
174
|
+
# 4b. Otherwise <loginname> runs the same command themselves (no sudo
|
|
175
|
+
# from ADMINUSER required, since they already are <loginname>):
|
|
176
|
+
pywrapper-install -p|--pgstart -n|--username <loginname>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Update an existing installation (no sudo required)
|
|
180
|
+
|
|
181
|
+
When the package is upgraded and a new `pywrapper.c` is bundled, use `-u/--update`
|
|
182
|
+
to recompile and reinstall all setuid binaries without needing `sudo`. The existing
|
|
183
|
+
`pgstart_*` binaries in `bin/` are used to perform the privileged operations:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
pywrapper-install -u|--update [-n|--username gdexdata] [-e|--envhome $ENVHOME]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Simple install (no sudo required, runs as current user)
|
|
190
|
+
|
|
191
|
+
Users who do not need the setuid mechanism can skip steps 2–4 and create a
|
|
192
|
+
direct symlink from `dsarch` to `setuid_dsarch`:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
pip install rda_python_dsarch
|
|
196
|
+
pywrapper-install -l|--link dsarch -s|--simple
|
|
197
|
+
pywrapper-install -l|--link all -s|--simple # or link all setuid_* entries at once
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Runtime flow
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
user runs: dsarch [args]
|
|
204
|
+
| (symlink -> pywrapper, setuid bit -> EUID=gdexdata)
|
|
205
|
+
pywrapper.c: execv(bin/setuid_dsarch, args)
|
|
206
|
+
setuid_dsarch: calls dsarch:main() as gdexdata
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Github
|
|
210
|
+
|
|
211
|
+
<https://github.com/NCAR/rda-python-setuid>
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "rda_python_setuid"
|
|
9
|
-
version = "
|
|
9
|
+
version = "3.0.0"
|
|
10
10
|
authors = [
|
|
11
11
|
{ name="Zaihua Ji", email="zji@ucar.edu" },
|
|
12
12
|
]
|
|
@@ -30,7 +30,7 @@ include-package-data = true
|
|
|
30
30
|
where = ["src"]
|
|
31
31
|
|
|
32
32
|
[tool.setuptools.package-data]
|
|
33
|
-
"rda_python_setuid" = ["pywrapper.c", "install.usg"]
|
|
33
|
+
"rda_python_setuid" = ["pywrapper.c", "install.usg", "setuid_setup.usg"]
|
|
34
34
|
|
|
35
35
|
[project.urls]
|
|
36
36
|
"Homepage" = "https://github.com/NCAR/rda-python-setuid"
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
##################################################################################
|
|
4
|
+
#
|
|
5
|
+
# Title: pywrapper-install
|
|
6
|
+
# Author: Zaihua Ji, zji@ucar.edu
|
|
7
|
+
# Date: 2025-05-11
|
|
8
|
+
# Purpose: Install helper for the pywrapper setuid C binary.
|
|
9
|
+
# Replaces the manual gcc/chmod/ln steps with a single command.
|
|
10
|
+
#
|
|
11
|
+
# Github: https://github.com/NCAR/rda-python-setuid.git
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# # 0. Display this user guide:
|
|
15
|
+
# pywrapper-install
|
|
16
|
+
#
|
|
17
|
+
# # 1. Compile and install pywrapper (run once per environment):
|
|
18
|
+
# pywrapper-install -c [-n gdexdata] [-e $ENVHOME]
|
|
19
|
+
#
|
|
20
|
+
# # 2. Create pgstart_USER entry so USER can run commands as themselves:
|
|
21
|
+
# pywrapper-install -p [-n zji] [-e $ENVHOME]
|
|
22
|
+
#
|
|
23
|
+
# # 3. Create a symlink so a program runs as CommonUser via pywrapper (setuid):
|
|
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]
|
|
28
|
+
#
|
|
29
|
+
# # 4. Simple install: symlink PROGRAM -> setuid_PROGRAM (no setuid, runs as current user):
|
|
30
|
+
# pywrapper-install -l myprog -s [-e $ENVHOME]
|
|
31
|
+
#
|
|
32
|
+
# # 5. Update existing installation (recompile and reinstall all setuid binaries):
|
|
33
|
+
# pywrapper-install -u [-n gdexdata] [-e $ENVHOME]
|
|
34
|
+
#
|
|
35
|
+
# Convention for wrapped programs:
|
|
36
|
+
# The target package must register its connector entry point with a setuid_ prefix:
|
|
37
|
+
# [project.scripts]
|
|
38
|
+
# "setuid_dsarch" = "rda_python_dsarch.dsarch:main"
|
|
39
|
+
# pip install places setuid_dsarch in the bin dir automatically.
|
|
40
|
+
# pywrapper-install --link dsarch creates the symlink dsarch -> pywrapper, so
|
|
41
|
+
# users invoking dsarch go through the setuid wrapper which execs setuid_dsarch
|
|
42
|
+
# as CommonUser.
|
|
43
|
+
# pywrapper-install --link dsarch --simple creates dsarch -> setuid_dsarch directly,
|
|
44
|
+
# skipping setuid; the program runs as the current user.
|
|
45
|
+
#
|
|
46
|
+
##################################################################################
|
|
47
|
+
|
|
48
|
+
import argparse
|
|
49
|
+
import os
|
|
50
|
+
import shutil
|
|
51
|
+
import subprocess
|
|
52
|
+
import sys
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_bindir():
|
|
56
|
+
"""Return the bin directory of the active Python environment."""
|
|
57
|
+
return os.path.dirname(os.path.abspath(sys.executable))
|
|
58
|
+
|
|
59
|
+
|
|
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')
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run(cmd):
|
|
66
|
+
print(" $", " ".join(cmd))
|
|
67
|
+
subprocess.run(cmd, check=True)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def show_usage():
|
|
71
|
+
usgfile = os.path.join(os.path.dirname(__file__), 'install.usg')
|
|
72
|
+
os.system("more " + usgfile)
|
|
73
|
+
sys.exit(0)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main():
|
|
77
|
+
|
|
78
|
+
parser = argparse.ArgumentParser(
|
|
79
|
+
description="Compile and install the pywrapper setuid C binary."
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
'-e', '--envhome', default=None,
|
|
83
|
+
help="Path to the venv root directory containing bin/ (default: parent of the current Python executable's bin/ dir)"
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
'-n', '--username', default=None,
|
|
87
|
+
help="User name to own the setuid binary (default: current login user for -p/--pgstart, gdexdata otherwise)"
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
'-s', '--simple', action='store_true',
|
|
91
|
+
help="Simple install: create symlink PROGRAM -> setuid_PROGRAM, skipping setuid (use with -l/--link)"
|
|
92
|
+
)
|
|
93
|
+
group = parser.add_mutually_exclusive_group()
|
|
94
|
+
group.add_argument(
|
|
95
|
+
'-c', '--compile', action='store_true',
|
|
96
|
+
help="Compile pywrapper.c and install bin/pywrapper as a setuid binary (run once per environment)"
|
|
97
|
+
)
|
|
98
|
+
group.add_argument(
|
|
99
|
+
'-p', '--pgstart', action='store_true',
|
|
100
|
+
help="Create pgstart_USER for running commands as USER (Mode 2)"
|
|
101
|
+
)
|
|
102
|
+
group.add_argument(
|
|
103
|
+
'-l', '--link', metavar='PROGRAM',
|
|
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"
|
|
109
|
+
)
|
|
110
|
+
args = parser.parse_args()
|
|
111
|
+
|
|
112
|
+
if not (args.compile or args.pgstart or args.link or args.update):
|
|
113
|
+
show_usage()
|
|
114
|
+
|
|
115
|
+
if args.username is None and not args.simple:
|
|
116
|
+
import pwd
|
|
117
|
+
if args.pgstart:
|
|
118
|
+
args.username = pwd.getpwuid(os.getuid()).pw_name
|
|
119
|
+
else:
|
|
120
|
+
args.username = 'gdexdata'
|
|
121
|
+
|
|
122
|
+
bindir = os.path.join(args.envhome, 'bin') if args.envhome else get_bindir()
|
|
123
|
+
pywrapper = os.path.join(bindir, 'pywrapper')
|
|
124
|
+
|
|
125
|
+
if 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))
|
|
159
|
+
else:
|
|
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))
|
|
172
|
+
else:
|
|
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))
|
|
180
|
+
|
|
181
|
+
elif args.pgstart:
|
|
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.
|
|
186
|
+
if not os.path.exists(pywrapper):
|
|
187
|
+
print("Error: {} not found. Run pywrapper-install --compile --username COMMONUSER first.".format(pywrapper))
|
|
188
|
+
sys.exit(1)
|
|
189
|
+
target = os.path.join(bindir, 'pgstart_{}'.format(args.username))
|
|
190
|
+
import pwd
|
|
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))
|
|
203
|
+
|
|
204
|
+
elif args.compile:
|
|
205
|
+
# Compile pywrapper.c and install pywrapper with setuid
|
|
206
|
+
src = get_c_source()
|
|
207
|
+
src_dest = os.path.join(bindir, 'pywrapper.c')
|
|
208
|
+
shutil.copy(src, src_dest)
|
|
209
|
+
print("Copied: {}".format(src_dest))
|
|
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))
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == '__main__': main()
|