Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions enlace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,32 @@
mounts it, serves it, and optionally gates it behind auth -- with zero boilerplate.
"""

from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as _version
from pathlib import Path

from enlace.base import (
AppConfig,
AuthConfig,
ConventionsConfig,
OAuthProviderConfig,
PlatformConfig,
StoreBackendConfig,
)
from enlace.compose import EnlaceConfigError, build_backend, create_app
from enlace.compose import EnlaceConfigError, Plugin, build_backend, create_app
from enlace.diagnose import DiagnosticReport, Issue, diagnose_app
from enlace.discover import ConventionDiscoverer, discover_apps
from enlace.serve import serve

__version__ = "0.0.1"
try:
__version__ = _version("enlace")
except PackageNotFoundError: # editable install with no metadata, etc.
__version__ = "0.0.0+local"

__all__ = [
"AppConfig",
"AuthConfig",
"ConventionsConfig",
"DiagnosticReport",
"Issue",
"OAuthProviderConfig",
"PlatformConfig",
"StoreBackendConfig",
"Plugin",
"ConventionDiscoverer",
"EnlaceConfigError",
"build_backend",
Expand Down
137 changes: 0 additions & 137 deletions enlace/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@

import json as json_module
import os
import secrets
import sys
from getpass import getpass
from pathlib import Path
from typing import Optional

Expand Down Expand Up @@ -178,7 +176,6 @@ def check(
config = _build_config(apps_dir, apps_dirs, app_dirs)

errors = config.check_conflicts()
errors.extend(_check_auth_env(config))
warnings: list[str] = []

if json:
Expand Down Expand Up @@ -368,135 +365,6 @@ def doctor(
sys.exit(1)


def _check_auth_env(config: PlatformConfig) -> list[str]:
"""Return a list of errors for missing auth-related env vars."""
errors: list[str] = []
auth = config.auth
if not auth.enabled:
return errors

if not os.environ.get(auth.signing_key_env):
errors.append(
f"Missing env var {auth.signing_key_env} (required when auth.enabled). "
"Run `enlace auth-generate-signing-key` to create one."
)

for app in config.apps:
if app.access == "protected:shared":
if not app.shared_password_env:
errors.append(
f"App '{app.name}' uses access='protected:shared' but has "
"no shared_password_env set in its config."
)
elif not os.environ.get(app.shared_password_env):
errors.append(
f"App '{app.name}': env var {app.shared_password_env} is unset. "
"Hash a password with `enlace auth-hash-password` and export it."
)

for name, provider in auth.oauth.items():
if not os.environ.get(provider.client_id_env):
errors.append(
f"OAuth provider '{name}': env var {provider.client_id_env} is unset."
)
if not os.environ.get(provider.client_secret_env):
errors.append(
f"OAuth provider '{name}': env var "
f"{provider.client_secret_env} is unset."
)

return errors


def auth_init():
"""Print a starter ``[auth]`` block for platform.toml."""
print(
"# Copy into your platform.toml and edit as needed.\n"
"[auth]\n"
"enabled = true\n"
'session_cookie_name = "enlace_session"\n'
"session_max_age_seconds = 86400\n"
'signing_key_env = "ENLACE_SIGNING_KEY"\n'
"secure_cookies = true\n"
"\n"
"[auth.stores]\n"
'backend = "file"\n'
'path = "~/.enlace/platform_store"\n'
"\n"
"[stores.user_data]\n"
'backend = "file"\n'
'path = "~/.enlace/user_data"\n'
)


def auth_generate_signing_key():
"""Print a URL-safe 32-byte signing key suitable for ENLACE_SIGNING_KEY."""
print(secrets.token_urlsafe(32))


def auth_hash_password():
"""Prompt for a password and print its argon2id hash."""
try:
from enlace.auth import hash_password
except ImportError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(2)
pw = getpass("Password: ")
confirm = getpass("Confirm: ")
if pw != confirm:
print("Passwords did not match.", file=sys.stderr)
sys.exit(1)
print(hash_password(pw))


def _load_session_store():
"""Build a SessionStore pointing at the configured platform store."""
from enlace.auth import SessionStore
from enlace.stores import make_file_store_factory

config = PlatformConfig.from_toml()
factory = make_file_store_factory(config.auth.stores.path)
return SessionStore(factory("sessions"))


def auth_list_sessions(*, json: bool = False):
"""List active sessions from the platform store.

Args:
json: Output as JSON.
"""
sessions = _load_session_store().list_all()
if json:
print(
json_module.dumps(
[{"session_id": sid, **info} for sid, info in sessions], indent=2
)
)
return
if not sessions:
print("No active sessions.")
return
for sid, info in sessions:
user = info.get("user_id") or "?"
email = info.get("email") or "-"
created = info.get("created_at") or 0
print(f"{sid} user={user} email={email} created_at={created:.0f}")


def auth_revoke_session(session_id: str):
"""Delete a session by id.

Args:
session_id: The session id to revoke.
"""
ok = _load_session_store().delete(session_id)
if ok:
print(f"Revoked {session_id}")
else:
print(f"No session named {session_id}", file=sys.stderr)
sys.exit(1)


def main():
argh.dispatch_commands(
[
Expand All @@ -506,11 +374,6 @@ def main():
list_apps,
diagnose,
doctor,
auth_init,
auth_generate_signing_key,
auth_hash_password,
auth_list_sessions,
auth_revoke_session,
]
)

Expand Down
31 changes: 0 additions & 31 deletions enlace/auth/__init__.py

This file was deleted.

46 changes: 0 additions & 46 deletions enlace/auth/cookies.py

This file was deleted.

Loading
Loading