Skip to content

Commit 32dd593

Browse files
authored
List jobs tweaks (#45)
* testing keeping flux-uri set * get rid of mini * print job spec * refactor multi-user auth to not use pam (instead OAuth2 similar) * submit needs to be string * total refactor to use database I am not happy with the current multi-user setup that tries to use pam, and still is not using good practices like oauth2/jwt tokens. I also do not like the idea of maintaining two modes of operation - one through a flux user and one via sudo running the server and launching jobs as the user. Instead, I think the flux restful server should be in charge of authenticating users, and using a local database that can be created by an entity like the flux operator, and then a secret to encrypt all communication. I have a lot of work to update tests, examples, and docs, and then I need to test with the flux operator, but this should at least be the start of the crux of work. Next I will work on fixing up the Python client to do the proper back and forth for an oauth2 token. * working on testing * update docs * ensure we have secret key in testing container * add migrations directory * update for launcher case * ensure we include completion fields Signed-off-by: vsoch <[email protected]>
1 parent a143c11 commit 32dd593

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1530
-504
lines changed

.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
flux-restful.db
2+
__pycache__
3+
.devcontainer
4+
.git
5+
migrations/versions/*

.github/workflows/python-tests.yaml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
needs: [prepare-container]
2323
services:
2424
api:
25-
image: ghcr.io/flux-framework/flux-restful-api:latest
25+
image: ghcr.io/flux-framework/flux-restful-api:test
2626
ports:
2727
- 5000:5000
2828
env:
@@ -44,18 +44,17 @@ jobs:
4444
env:
4545
INSTALL_BRANCH: ${{ needs.prepare-container.outputs.branch }}
4646
INSTALL_REPO: ${{ github.repository }}
47-
FLUX_USER: fluxuser
48-
FLUX_TOKEN: "12345"
49-
FLUX_REQUIRE_AUTH: true
50-
image: ghcr.io/flux-framework/flux-restful-api:latest
47+
image: ghcr.io/flux-framework/flux-restful-api:test
5148
ports:
5249
- 5000:5000
5350
steps:
5451
- uses: actions/checkout@v3
5552
- name: Run tests with auth
5653
run: |
57-
cd clients/python
58-
pip install -e .[all]
5954
export FLUX_USER=fluxuser
6055
export FLUX_TOKEN=12345
56+
export FLUX_REQUIRE_AUTH=true
57+
export FLUX_SECRET_KEY=notsecrethoo
58+
cd clients/python
59+
pip install -e .[all]
6160
pytest -xs flux_restful_client/tests/test_api.py

.github/workflows/tests.yaml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ jobs:
1717
runs-on: ubuntu-latest
1818
needs: [prepare-container]
1919
container:
20-
image: ghcr.io/flux-framework/flux-restful-api:latest
20+
image: ghcr.io/flux-framework/flux-restful-api:test
2121
ports:
2222
- 5000
2323
env:
2424
INSTALL_BRANCH: ${{ needs.prepare-container.outputs.branch }}
2525
INSTALL_REPO: ${{ github.repository }}
26+
FLUX_SECRET_KEY: notsosecrethoo
27+
FLUX_USER: fluxuser
28+
FLUX_TOKEN: "12345"
2629
steps:
2730
- uses: actions/checkout@v3
2831
- name: Install Dependencies (in case changes)
@@ -32,20 +35,17 @@ jobs:
3235
# Tests for the API with auth disabled
3336
flux start pytest -xs tests/test_api.py
3437
35-
# Tests for the API with single user auth
36-
export FLUX_REQUIRE_AUTH=true
37-
export TEST_AUTH=true
38+
# Create main user in database
3839
export FLUX_USER=fluxuser
3940
export FLUX_TOKEN=12345
40-
flux start pytest -xs tests/test_api.py
4141
42-
# Tests for the API with multi-user auth, but fail because user not created
43-
unset FLUX_USER
44-
unset FLUX_TOKEN
45-
export TEST_PAM_AUTH=true
46-
export TEST_PAM_AUTH_FAIL=true
47-
export FLUX_ENABLE_PAM=true
48-
flux start pytest -xs tests/test_api.py
42+
alembic revision --autogenerate -m "Create intital tables"
43+
alembic upgrade head
44+
python3 app/db/init_db.py init
4945
50-
# TODO how to test pam in this mode?
51-
# We would need to start flux as flux and run tests as a user
46+
# Require auth and shared secret key
47+
export FLUX_SECRET_KEY=notsosecrethoo
48+
export FLUX_REQUIRE_AUTH=true
49+
export TEST_AUTH=true
50+
51+
flux start pytest -xs tests/test_api.py

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
flux-restful.db
12
env
23
.env
34
__pycache__

Dockerfile

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,14 @@ FROM fluxrm/flux-sched:focal
44

55
# This must be set to work (username / token set won't trigger it alone)
66
ARG use_auth
7-
ARG user="fluxuser"
8-
ARG token="12345"
9-
ARG port="5000"
10-
ARG host="0.0.0.0"
11-
ARG workers="1"
127
LABEL maintainer="Vanessasaurus <@vsoch>"
138

14-
ENV FLUX_USER=${user}
15-
ENV FLUX_TOKEN=${token}
9+
ENV FLUX_USER=${user:-fluxuser}
10+
ENV FLUX_TOKEN=${token:-12345}
1611
ENV FLUX_REQUIRE_AUTH=${use_auth}
17-
ENV PORT=${port}
18-
ENV HOST=${host}
19-
ENV WORKERS=${workers}
12+
ENV PORT=${port:-5000}
13+
ENV HOST=${host:-0.0.0.0}
14+
ENV WORKERS=${workers:-1}
2015

2116
USER root
2217
RUN apt-get update
@@ -26,7 +21,7 @@ EXPOSE ${port}
2621
ENV PYTHONPATH=/usr/lib/flux/python3.8:/code
2722

2823
# For easier Python development, and install time for timed commands
29-
RUN python3 -m pip install -r /requirements.txt && \
24+
RUN pip install -r /requirements.txt && \
3025
apt-get update && apt-get install -y time && \
3126
apt-get clean && \
3227
apt-get autoremove && \

README.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ tool to generate a contributors graphic below.
4545
<!-- ALL-CONTRIBUTORS-LIST:END -->
4646

4747

48-
## TODO
49-
50-
- Interface view with nice job table
51-
- We can put additional assets for the server in [data](data), not sure what those are yet!
52-
5348
#### Release
5449

5550
SPDX-License-Identifier: LGPL-3.0

alembic.ini

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# A generic, single database configuration.
2+
3+
[alembic]
4+
# path to migration scripts
5+
script_location = migrations
6+
7+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8+
# Uncomment the line below if you want the files to be prepended with date and time
9+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
10+
# for all available tokens
11+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
12+
13+
# sys.path path, will be prepended to sys.path if present.
14+
# defaults to the current working directory.
15+
prepend_sys_path = .
16+
17+
# timezone to use when rendering the date within the migration file
18+
# as well as the filename.
19+
# If specified, requires the python-dateutil library that can be
20+
# installed by adding `alembic[tz]` to the pip requirements
21+
# string value is passed to dateutil.tz.gettz()
22+
# leave blank for localtime
23+
# timezone =
24+
25+
# max length of characters to apply to the
26+
# "slug" field
27+
# truncate_slug_length = 40
28+
29+
# set to 'true' to run the environment during
30+
# the 'revision' command, regardless of autogenerate
31+
# revision_environment = false
32+
33+
# set to 'true' to allow .pyc and .pyo files without
34+
# a source .py file to be detected as revisions in the
35+
# versions/ directory
36+
# sourceless = false
37+
38+
# version location specification; This defaults
39+
# to alembic/versions. When using multiple version
40+
# directories, initial revisions must be specified with --version-path.
41+
# The path separator used here should be the separator specified by "version_path_separator" below.
42+
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
43+
44+
# version path separator; As mentioned above, this is the character used to split
45+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
46+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
47+
# Valid values for version_path_separator are:
48+
#
49+
# version_path_separator = :
50+
# version_path_separator = ;
51+
# version_path_separator = space
52+
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
53+
54+
# the output encoding used when revision files
55+
# are written from script.py.mako
56+
# output_encoding = utf-8
57+
58+
sqlalchemy.url = sqlite:///./flux-restful.db
59+
60+
61+
[post_write_hooks]
62+
# post_write_hooks defines scripts or Python functions that are run
63+
# on newly generated revision scripts. See the documentation for further
64+
# detail and examples
65+
66+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
67+
# hooks = black
68+
# black.type = console_scripts
69+
# black.entrypoint = black
70+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
71+
72+
# Logging configuration
73+
[loggers]
74+
keys = root,sqlalchemy,alembic
75+
76+
[handlers]
77+
keys = console
78+
79+
[formatters]
80+
keys = generic
81+
82+
[logger_root]
83+
level = WARN
84+
handlers = console
85+
qualname =
86+
87+
[logger_sqlalchemy]
88+
level = WARN
89+
handlers =
90+
qualname = sqlalchemy.engine
91+
92+
[logger_alembic]
93+
level = INFO
94+
handlers =
95+
qualname = alembic
96+
97+
[handler_console]
98+
class = StreamHandler
99+
args = (sys.stderr,)
100+
level = NOTSET
101+
formatter = generic
102+
103+
[formatter_generic]
104+
format = %(levelname)-5.5s [%(name)s] %(message)s
105+
datefmt = %H:%M:%S

app/core/config.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import logging
22
import os
33
import re
4+
import secrets
45
import shlex
6+
import string
57

68
from pydantic import BaseSettings
79

@@ -58,23 +60,39 @@ def parse_option_flags(flags, prefix="-o"):
5860
return values
5961

6062

63+
def generate_secret_key(length=32):
64+
"""
65+
Generate a secret key to encrypt, if one not provided.
66+
"""
67+
alphabet = string.ascii_letters + string.digits
68+
return "".join(secrets.choice(alphabet) for i in range(length))
69+
70+
6171
class Settings(BaseSettings):
6272
"""
6373
Basic settings and defaults for the Flux RESTFul API
6474
"""
6575

6676
app_name: str = "Flux RESTFul API"
77+
api_version: str = "v1"
6778

6879
# These map to envars, e.g., FLUX_USER
6980
has_gpus: bool = get_bool_envar("FLUX_HAS_GPUS")
70-
enable_pam: bool = get_bool_envar("FLUX_ENABLE_PAM")
7181

7282
# Assume there is at least one node!
7383
flux_nodes: int = get_int_envar("FLUX_NUMBER_NODES", 1)
84+
require_auth: bool = get_bool_envar("FLUX_REQUIRE_AUTH")
7485

86+
# If you change this, also change in alembic.ini
87+
db_file: str = "sqlite:///./flux-restful.db"
7588
flux_user: str = os.environ.get("FLUX_USER")
7689
flux_token: str = os.environ.get("FLUX_TOKEN")
77-
require_auth: bool = get_bool_envar("FLUX_REQUIRE_AUTH")
90+
secret_key: str = os.environ.get("FLUX_SECRET_KEY") or generate_secret_key()
91+
92+
# Expires in 10 hours
93+
access_token_expires_minutes: int = get_int_envar(
94+
"FLUX_ACCESS_TOKEN_EXPIRES_MINUTES", 600
95+
)
7896

7997
# Default server option flags
8098
option_flags: dict = get_option_flags("FLUX_OPTION_FLAGS")

app/core/security.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from datetime import datetime, timedelta
2+
from typing import Any, Union
3+
4+
from jose import jwt
5+
from passlib.context import CryptContext
6+
7+
from app.core.config import settings
8+
9+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
10+
11+
12+
ALGORITHM = "HS256"
13+
14+
15+
def create_access_token(
16+
subject: Union[str, Any], expires_delta: timedelta = None
17+
) -> str:
18+
if expires_delta:
19+
expire = datetime.utcnow() + expires_delta
20+
else:
21+
expire = datetime.utcnow() + timedelta(
22+
minutes=settings.access_token_expires_minutes
23+
)
24+
to_encode = {"exp": expire, "sub": str(subject)}
25+
return jwt.encode(to_encode, settings.secret_key, algorithm=ALGORITHM)
26+
27+
28+
def verify_password(plain_password: str, hashed_password: str) -> bool:
29+
return pwd_context.verify(plain_password, hashed_password)
30+
31+
32+
def get_password_hash(password: str) -> str:
33+
return pwd_context.hash(password)

app/crud/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .user import user # noqa

0 commit comments

Comments
 (0)