Add authenticated dashboard and route deployment through Kong
This commit is contained in:
82
defense_sim_v2/scripts/bootstrap_auth.py
Normal file
82
defense_sim_v2/scripts/bootstrap_auth.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate stable local auth secrets. Nothing privileged is emitted to stdout."""
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
AUTH_DIR = ROOT / 'deploy/supabase'
|
||||
|
||||
|
||||
def read_env(path):
|
||||
return dict(line.split('=', 1) for line in path.read_text().splitlines() if line and not line.startswith('#'))
|
||||
|
||||
|
||||
def jwt(secret, role):
|
||||
def encode(value):
|
||||
return base64.urlsafe_b64encode(json.dumps(value, separators=(',', ':')).encode()).rstrip(b'=').decode()
|
||||
now = int(time.time())
|
||||
message = encode({'alg': 'HS256', 'typ': 'JWT'}) + '.' + encode({'role': role, 'iss': 'supabase', 'iat': now, 'exp': now + 10 * 365 * 86400})
|
||||
signature = base64.urlsafe_b64encode(hmac.new(secret.encode(), message.encode(), hashlib.sha256).digest()).rstrip(b'=').decode()
|
||||
return message + '.' + signature
|
||||
|
||||
|
||||
def write_private(path, content):
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(fd, 'w') as output:
|
||||
output.write(content)
|
||||
path.chmod(0o600)
|
||||
|
||||
|
||||
def main():
|
||||
env_path = AUTH_DIR / '.env'
|
||||
if not env_path.exists():
|
||||
secret = secrets.token_hex(32)
|
||||
demo_path = ROOT / 'DEMO_USER.txt'
|
||||
demo = dict(line.split(': ', 1) for line in demo_path.read_text().splitlines() if ': ' in line) if demo_path.exists() else {}
|
||||
values = {
|
||||
'POSTGRES_PASSWORD': secrets.token_hex(24),
|
||||
'JWT_SECRET': secret,
|
||||
'SUPABASE_ANON_KEY': jwt(secret, 'anon'),
|
||||
'SUPABASE_SERVICE_KEY': jwt(secret, 'service_role'),
|
||||
'SUPABASE_PORT': os.getenv('SUPABASE_PORT', '54321'),
|
||||
'KONG_BIND_ADDRESS': os.getenv('KONG_BIND_ADDRESS', '0.0.0.0'),
|
||||
'SITE_URL': os.getenv('SITE_URL', 'https://defense.naori.cloud'),
|
||||
'DEMO_EMAIL': demo.get('Username', 'demo@defense.naori.cloud'),
|
||||
'DEMO_PASSWORD': demo.get('Password', secrets.token_urlsafe(18)),
|
||||
}
|
||||
write_private(env_path, ''.join(f'{key}={value}\n' for key, value in values.items()))
|
||||
print('Generated auth secrets and demo credentials in deploy/supabase/.env')
|
||||
values = read_env(env_path)
|
||||
config = {
|
||||
'_format_version': '2.1', '_transform': True,
|
||||
'consumers': [
|
||||
{'username': 'anon', 'keyauth_credentials': [{'key': values['SUPABASE_ANON_KEY']}]},
|
||||
{'username': 'service_role', 'keyauth_credentials': [{'key': values['SUPABASE_SERVICE_KEY']}]},
|
||||
],
|
||||
'services': [
|
||||
{'name': name, 'url': url, 'routes': [{'name': name, 'paths': [path], 'strip_path': True}],
|
||||
'plugins': [{'name': 'cors'}, {'name': 'key-auth', 'config': {'hide_credentials': False}}]}
|
||||
for name, url, path in [('auth', 'http://auth:9999/', '/auth/v1/'), ('rest', 'http://rest:3000/', '/rest/v1/')]
|
||||
],
|
||||
}
|
||||
config['services'].extend([
|
||||
{'name': 'simulation', 'url': 'http://host.docker.internal:8000',
|
||||
'routes': [{'name': 'simulation', 'paths': ['/socket.io/'], 'strip_path': False}],
|
||||
'read_timeout': 60000, 'write_timeout': 60000},
|
||||
{'name': 'frontend', 'url': 'http://frontend:80',
|
||||
'routes': [{'name': 'frontend', 'paths': ['/'], 'strip_path': False}]},
|
||||
])
|
||||
# Kong runs as UID 1337; its bind-mounted config must be readable inside the container.
|
||||
write_private(AUTH_DIR / 'kong.json.local', json.dumps(config, indent=2))
|
||||
(AUTH_DIR / 'kong.json.local').chmod(0o644)
|
||||
write_private(ROOT / 'deploy/auth.env.local', f"SUPABASE_URL=http://127.0.0.1:{values['SUPABASE_PORT']}\nSUPABASE_ANON_KEY={values['SUPABASE_ANON_KEY']}\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
45
defense_sim_v2/scripts/create_demo_user.py
Normal file
45
defense_sim_v2/scripts/create_demo_user.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Provision and verify the demo account without replacing an existing password."""
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from bootstrap_auth import AUTH_DIR, read_env
|
||||
|
||||
|
||||
def request(url, key, method='GET', payload=None):
|
||||
headers = {'apikey': key, 'Authorization': f'Bearer {key}', 'Content-Type': 'application/json'}
|
||||
body = json.dumps(payload).encode() if payload is not None else None
|
||||
with urllib.request.urlopen(urllib.request.Request(url, data=body, headers=headers, method=method), timeout=15) as response:
|
||||
return json.load(response)
|
||||
|
||||
|
||||
def main():
|
||||
values = read_env(AUTH_DIR / '.env')
|
||||
url = f"http://127.0.0.1:{values['SUPABASE_PORT']}/auth/v1"
|
||||
credentials = {'email': values['DEMO_EMAIL'], 'password': values['DEMO_PASSWORD']}
|
||||
existing = None
|
||||
page = 1
|
||||
while True:
|
||||
users = request(f'{url}/admin/users?page={page}&per_page=100', values['SUPABASE_SERVICE_KEY']).get('users', [])
|
||||
existing = next((user for user in users if user.get('email') == credentials['email']), None)
|
||||
if existing or len(users) < 100:
|
||||
break
|
||||
page += 1
|
||||
if not existing:
|
||||
request(f'{url}/admin/users', values['SUPABASE_SERVICE_KEY'], 'POST', {
|
||||
**credentials, 'email_confirm': True, 'user_metadata': {'name': 'Demo user'},
|
||||
})
|
||||
print('Created demo account.')
|
||||
session = request(f'{url}/token?grant_type=password', values['SUPABASE_ANON_KEY'], 'POST', credentials)
|
||||
if not session.get('access_token') or not session.get('user', {}).get('email') == credentials['email']:
|
||||
raise RuntimeError('Demo login verification failed')
|
||||
print('Demo password login verified. Credentials are stored in deploy/supabase/.env.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except urllib.error.HTTPError as error:
|
||||
print(f'Demo account setup failed (HTTP {error.code}).', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user