diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a530c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.venv/ +__pycache__/ +*.pyc +/config.json +*.sock diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 0000000..6eab5a5 --- /dev/null +++ b/dev/README.md @@ -0,0 +1,63 @@ +# Local dev setup + +Runs steakweb on your machine against a throwaway Postgres, no prod DB or SAML +IdP needed. Enough to click through the activation form and the directory. + +`dev/schema.sql` is the production schema (posted by supersat in PR #2), plus a +primary key on `extn` that create_extn's "already taken" handling relies on. + +Everything dev-only is gated on `STEAKWEB_DEV`, so it's off in prod. + +## Run it + +1. Start Postgres (loads schema + seed on first boot, listens on `127.0.0.1:5433`): + + docker compose -f dev/docker-compose.yml up -d + +2. Python deps: + + python3 -m venv .venv + .venv/bin/pip install -r requirements.txt + + `python3-saml` may need system libs to build (Debian/Ubuntu: + `sudo apt install pkg-config libxml2-dev libxmlsec1-dev libxmlsec1-openssl`). + It's imported but unused in dev. + +3. Config (`config.json` is gitignored): + + cp dev/config.dev.json config.json + +4. Run: + + STEAKWEB_DEV=1 .venv/bin/python steakweb.py + + Serves on `http://127.0.0.1:8080`. If 8080's taken, set `STEAKWEB_DEV_PORT=8099`. + +In prod (`STEAKWEB_DEV` unset) it serves over the unix socket like before. The +TCP port, `/dev/login`, and the non-secure cookie are dev-only. + +## What to check + +Swap `8080` for your port below. + +Directory, no login: + +- `/directory` — the 6 published rows, with the unpublished "Hidden Back Office" + (4040) hidden and the ` & "Bobby"', 1010, '000000000005', true, NULL, true), -- HTML metachars + (6999, 'Edge Of Range', 1010, '000000000006', true, NULL, true), + (8888, 'Legacy Trunk (out of range)', 1010, '000000000007', true, NULL, true); -- out-of-range legacy diff --git a/steakweb.py b/steakweb.py index 6b43db0..0a350c7 100644 --- a/steakweb.py +++ b/steakweb.py @@ -51,7 +51,32 @@ def check_auth_isadmin(session): def gen_sip_pw(): return secrets.token_urlsafe(8) - + +### DEV ONLY ------------------------------------------------------------------ +# Everything guarded by STEAKWEB_DEV is completely inert in production: when the +# env var is unset, DEV_MODE is False, the dev route is never registered, and +# none of this code path runs. It exists only so a developer can exercise the +# authenticated flows (homepage, create_extn, ...) locally without a real +# Authentik/SAML IdP. See dev/README.md. +DEV_MODE = bool(os.environ.get('STEAKWEB_DEV')) + +async def dev_login(request): + # DEV ONLY: forge a logged-in session (no IdP). Mirrors the shape that + # saml_acs() produces: uid, attributes (with a goauthentik username), iat. + # Append ?admin=1 to also join the "Extension Admins" group. + session = await aiohttp_session.new_session(request) + session['uid'] = os.environ.get('STEAKWEB_DEV_UID', '1010') + username = os.environ.get('STEAKWEB_DEV_USER', 'devuser') + attributes = { + 'http://schemas.goauthentik.io/2021/02/saml/username': [username], + } + if 'admin' in request.query: + attributes['http://schemas.xmlsoap.org/claims/Group'] = ['Extension Admins'] + session['attributes'] = attributes + session['iat'] = time.time() + raise web.HTTPFound('/') +### END DEV ONLY -------------------------------------------------------------- + ### get request handlers async def homepage(request): @@ -156,7 +181,7 @@ async def create_extn(request): await init_db_pool() name = data.get('name', '') - publish = 't' if data.get('publish') else 'f' + publish = bool(data.get('publish')) if data['type'] == 'sip': switch = 11 authcode = gen_sip_pw() @@ -187,7 +212,7 @@ async def publish_extn(request): if check_auth_isadmin(session): n = await dbconn.execute("UPDATE registered_extensions SET publish = $2 WHERE extn = $1", int(data['extn']), data.get('published', '0')== '1') else: - n = await dbconn.execute("UPDATE registered_extensions SET publish = $2 WHERE extn = $1 AND userid = $3", int(data['extn']), data.get('publish', '1') == '1', int(session['uid'])) + n = await dbconn.execute("UPDATE registered_extensions SET publish = $2 WHERE extn = $1 AND userid = $3", int(data['extn']), data.get('published', '0') == '1', int(session['uid'])) if n != 'UPDATE 1': session['error'] = 'Could not change directory name; contact support' @@ -256,7 +281,10 @@ async def init_saml_settings(): aiohttp_session.setup(app, EncryptedCookieStorage( cookiekey, cookie_name='session', - secure=True, + # DEV ONLY: a Secure cookie is dropped by browsers over plain http, + # which would break /dev/login over http://localhost. In production + # (DEV_MODE False) this stays True, exactly as before. + secure=not DEV_MODE, samesite='strict' )) @@ -277,15 +305,32 @@ async def init_saml_settings(): app.add_routes([web.static('/static', os.path.join(os.getcwd(), 'static'))]) - asyncio.run(init_saml_settings()) + if DEV_MODE: + # DEV ONLY: forge-a-session login so authenticated flows work without an IdP. + app.add_routes([web.get('/dev/login', dev_login)]) - try: - os.unlink(socketpath) - except: - pass - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.bind(socketpath) - os.chmod(socketpath, 0o666) - web.run_app(app, sock=sock) + if DEV_MODE: + # DEV ONLY: the production init hits the remote IdP metadata endpoint + # (identity.shady.tel); skip it since SAML is unused in dev. + print('STEAKWEB_DEV set: skipping SAML IdP metadata fetch') + else: + asyncio.run(init_saml_settings()) + + if DEV_MODE: + # DEV ONLY: bind a TCP port so a browser/curl can reach the app directly + # (production serves over the unix socket in the else branch below). + host = os.environ.get('STEAKWEB_DEV_HOST', '127.0.0.1') + port = int(os.environ.get('STEAKWEB_DEV_PORT', '8080')) + print(f'STEAKWEB_DEV set: serving on http://{host}:{port}') + web.run_app(app, host=host, port=port) + else: + try: + os.unlink(socketpath) + except: + pass + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.bind(socketpath) + os.chmod(socketpath, 0o666) + web.run_app(app, sock=sock) # vim: set ts=4 sw=4 expendtab