Skip to content

Commit e6dc49c

Browse files
authored
Test signing (#50)
* first draft (for testing only) to sign job payload Signed-off-by: vsoch <[email protected]>
1 parent 16999ec commit e6dc49c

File tree

16 files changed

+175
-41
lines changed

16 files changed

+175
-41
lines changed

.devcontainer/Dockerfile

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM ghcr.io/rse-ops/accounting:app-latest
1+
FROM ghcr.io/rse-ops/pokemon:app-latest
22

33
LABEL maintainer="Vanessasaurus <@vsoch>"
44

@@ -28,22 +28,9 @@ RUN python3 -m venv /env && \
2828

2929
RUN mkdir -p /run/flux /var/lib/flux mkdir /etc/flux/system/cron.d /mnt/curve && \
3030
flux keygen /mnt/curve/curve.cert && \
31-
# This probably needs to be done as flux user?
32-
flux account create-db && \
33-
flux account add-bank root 1 && \
34-
flux account add-bank --parent-bank=root user_bank 1 && \
3531
# These need to be owned by flux
3632
chown -R flux /run/flux /var/lib/flux /mnt/curve && \
3733
# flux-imp needs setuid permission
3834
chmod u+s /usr/libexec/flux/flux-imp
39-
# flux account add-user --username=fluxuser --bank=user_bank
40-
41-
COPY ./example/multi-user/flux.service /etc/systemd/system/flux.service
42-
COPY ./example/multi-user/broker.toml /etc/flux/system/conf.d/broker.toml
43-
COPY ./example/multi-user/imp.toml /etc/flux/imp/conf.d/imp.toml
44-
45-
RUN chmod 4755 /usr/libexec/flux/flux-imp \
46-
&& chmod 0644 /etc/flux/imp/conf.d/imp.toml \
47-
&& chmod 0644 /etc/flux/system/conf.d/broker.toml
4835

4936
ENV PATH=/env/bin:${PATH}

.github/workflows/tests.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,8 @@ jobs:
4848
export FLUX_REQUIRE_AUTH=true
4949
export TEST_AUTH=true
5050
51-
flux start pytest -xs tests/test_api.py
51+
# This needs to be run as the flux instance owner
52+
# E.g., if it's flux
53+
# sudo -u flux flux start pytest -xs tests/test_api.py
54+
whoami
55+
sudo -u fluxuser flux start pytest -xs tests/test_api.py

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and **Merged pull requests**. Critical items to know are:
1414
The versions coincide with releases on pip. Only major versions will be released as tags on Github.
1515

1616
## [0.0.x](https://github.com/flux-framework/flux-restful-api/tree/main) (0.0.x)
17+
- Add better multi-user mode - running jobs on behalf of user (0.1.12)
1718
- Restore original rpc to get job info (has more information) (0.1.11)
1819
- Refactor of FLux Restful to use a database and OAauth2 (0.1.0)
1920
- Support for basic PAM authentication (0.0.11)

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.1
1+
0.1.12

app/core/security.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,36 @@
1313

1414

1515
def create_access_token(
16-
subject: Union[str, Any], expires_delta: timedelta = None
16+
subject: Union[str, Any], expires_delta: timedelta = None, secret_key=None
1717
) -> str:
18+
"""
19+
Create a jwt access token.
20+
21+
We either use the user's secret key (which is hashed) or fall
22+
back to the server set secret key.
23+
"""
24+
# Use a user secret key, if they have one.
25+
# Otherwise fall back to server secret key
26+
secret_key = secret_key or settings.secret_key
1827
if expires_delta:
1928
expire = datetime.utcnow() + expires_delta
2029
else:
2130
expire = datetime.utcnow() + timedelta(
2231
minutes=settings.access_token_expires_minutes
2332
)
2433
to_encode = {"exp": expire, "sub": str(subject)}
25-
return jwt.encode(to_encode, settings.secret_key, algorithm=ALGORITHM)
34+
return jwt.encode(to_encode, secret_key, algorithm=ALGORITHM)
2635

2736

2837
def verify_password(plain_password: str, hashed_password: str) -> bool:
38+
"""
39+
Verify the password
40+
"""
2941
return pwd_context.verify(plain_password, hashed_password)
3042

3143

3244
def get_password_hash(password: str) -> str:
45+
"""
46+
Note we aren't providing a salt here, so the same password can generate different.
47+
"""
3348
return pwd_context.hash(password)

app/crud/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ def get(self, db: Session, id: Any) -> Optional[ModelType]:
2929
def get_multi(
3030
self, db: Session, *, skip: int = 0, limit: int = 100
3131
) -> List[ModelType]:
32+
if limit is None:
33+
return db.query(self.model).offset(skip).all()
3234
return db.query(self.model).offset(skip).limit(limit).all()
3335

3436
def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:

app/library/flux.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,67 @@
22
import os
33
import re
44
import shlex
5+
import subprocess
56
import time
67

78
import flux
89
import flux.job
910

1011
from app.core.config import settings
1112

13+
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14+
submit_script = os.path.join(root, "scripts", "submit-job.py")
1215

13-
def submit_job(handle, jobspec, user):
16+
17+
class FakeJob:
18+
def __init__(self, jobid):
19+
self.jobid = jobid
20+
21+
def get_id(self):
22+
return self.jobid
23+
24+
25+
def submit_job(handle, fluxjob, user):
1426
"""
15-
Handle to submit a job, either with flux job submit or on behalf of user.
27+
Submit the job on behalf of user.
1628
"""
1729
if user and hasattr(user, "user_name"):
1830
print(f"User submitting job {user.user_name}")
31+
user = user.user_name
1932
elif user and isinstance(user, str):
2033
print(f"User submitting job {user}")
21-
return flux.job.submit_async(handle, jobspec)
34+
35+
# If we don't have auth enabled, submit in single-user mode
36+
if not settings.require_auth:
37+
print("Submit in single-user mode.")
38+
return flux.job.submit_async(handle, fluxjob)
39+
40+
# Update the payload for the correct user
41+
# Use helper script to sign payload
42+
payload = json.dumps(fluxjob.jobspec)
43+
# payload['HOME'] =
44+
45+
# We ideally need to pipe the payload into flux python
46+
try:
47+
ps = subprocess.Popen(("echo", payload), stdout=subprocess.PIPE)
48+
output = subprocess.check_output(
49+
("sudo", "-E", "-u", user, "flux", "python", submit_script),
50+
stdin=ps.stdout,
51+
env=os.environ,
52+
)
53+
ps.wait()
54+
55+
# A flux start without sudo -u flux can cause this
56+
# This will be caught and returned to the user
57+
except PermissionError as e:
58+
raise ValueError(
59+
f"Permission error: {e}! Are you running the instance as the flux user?"
60+
)
61+
62+
jobid = output.decode("utf-8").strip()
63+
print("Submit job {jobid}")
64+
job = FakeJob(jobid)
65+
return job
2266

2367

2468
def validate_submit_kwargs(kwargs, envars=None, runtime=None):
@@ -98,8 +142,9 @@ def prepare_job(user, kwargs, runtime=0, workdir=None, envars=None):
98142
# Set an attribute about the owning user
99143
if user and hasattr(user, "user_name"):
100144
fluxjob.setattr("user", user.user_name)
101-
elif isinstance(user, str):
102-
fluxjob.setattr("user", user)
145+
user = user.user_name
146+
147+
fluxjob.setattr("user", user)
103148

104149
# Set a provided working directory
105150
print(f"⭐️ Workdir provided: {workdir}")

app/models/user.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ class User(Base):
88
id = Column(Integer, primary_key=True, index=True)
99
user_name = Column(String, unique=True, index=True, nullable=False)
1010
hashed_password = Column(String, nullable=False)
11+
12+
# These are only used to encode and validate payloads, and
13+
# can be reset at any time. Not related to the hashed password
14+
secret_key = Column(String, nullable=True)
1115
is_active = Column(Boolean(), default=True)
1216
is_superuser = Column(Boolean(), default=False)
1317
jobs = relationship("Job", back_populates="owner")

app/routers/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,10 @@ async def submit_job(request: Request, user=user_auth):
190190
include everything in this function instead of having separate
191191
functions.
192192
"""
193-
print(f"User for submit is {user}")
194193
from app.main import app
195194

195+
print(f"User for submit is {user}")
196+
196197
# This can bork if no payload is provided
197198
try:
198199
payload = await request.json()

app/routers/views.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@ async def submit_job_post(request: Request, user=user_auth):
163163
Receive data posted (submit) to the form.
164164
"""
165165
print(user)
166-
from app.main import app
167166

168167
messages = []
169168
form = SubmitForm(request)
@@ -177,7 +176,7 @@ async def submit_job_post(request: Request, user=user_auth):
177176
launcher.launch(form.kwargs, workdir=form.workdir, user=user)
178177
)
179178
else:
180-
return submit_job_helper(request, app, form, user=user)
179+
return submit_job_helper(request, form, user=user)
181180
else:
182181
print("🍒 Submit form is NOT valid!")
183182
return templates.TemplateResponse(
@@ -192,10 +191,12 @@ async def submit_job_post(request: Request, user=user_auth):
192191
)
193192

194193

195-
def submit_job_helper(request, app, form, user):
194+
def submit_job_helper(request, form, user):
196195
"""
197196
A helper to submit a flux job (not a launcher)
198197
"""
198+
from app.main import app
199+
199200
# Submit the job and return the ID, but allow for error
200201
# Prepare the flux job! We don't support envars here yet
201202
try:

0 commit comments

Comments
 (0)