app: initial version with templating
This commit is contained in:
parent
28b2d89b06
commit
686c9efc2c
56
frontpage/app.py
Normal file
56
frontpage/app.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""Main entrypoint."""
|
||||
import argparse
|
||||
import secrets
|
||||
|
||||
from flask import Flask
|
||||
from flask_pyoidc import OIDCAuthentication
|
||||
from flask_pyoidc.provider_configuration import ClientMetadata, ProviderConfiguration
|
||||
|
||||
from frontpage import config, routes
|
||||
from frontpage.config import OidcConfig
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def _init_oidc(cfg: OidcConfig) -> OIDCAuthentication:
|
||||
if cfg.client_secret_file is not None:
|
||||
with open(cfg.client_secret_file, encoding="UTF-8") as f:
|
||||
secret = f.read()
|
||||
else:
|
||||
secret = cfg.client_secret
|
||||
print(secret)
|
||||
client = ClientMetadata(client_id=cfg.client_id, client_secret=secret)
|
||||
|
||||
# Ensure we have the minimum set of scopes for things to work.
|
||||
scopes = {"openid"}
|
||||
scopes.update(cfg.scopes)
|
||||
|
||||
auth_params = {"scope": list(scopes)}
|
||||
provider = ProviderConfiguration(
|
||||
issuer=cfg.issuer, client_metadata=client, auth_request_params=auth_params
|
||||
)
|
||||
|
||||
auth = OIDCAuthentication({"default": provider}, app)
|
||||
return auth
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Entrypoint.
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-c", "--config", metavar="FILE", help="configuration file", required=True
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
cfg = config.load(args.config)
|
||||
auth = _init_oidc(cfg.oidc)
|
||||
|
||||
app.register_blueprint(routes.register(auth, "default"))
|
||||
|
||||
app.config.update({"DEBUG": cfg.app.debug, "SECRET_KEY": secrets.token_hex()})
|
||||
app.config["user_config"] = cfg
|
||||
|
||||
# Reloader doesn't work on NixOS
|
||||
app.run(use_reloader=False)
|
85
frontpage/config.py
Normal file
85
frontpage/config.py
Normal file
@ -0,0 +1,85 @@
|
||||
"""Configuration classes and utilities."""
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import toml
|
||||
import dacite
|
||||
|
||||
from flask import current_app
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoreConfig:
|
||||
"""Core application configuration."""
|
||||
|
||||
debug: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class OidcConfig:
|
||||
"""OIDC client configuration."""
|
||||
|
||||
issuer: str
|
||||
client_id: str
|
||||
client_secret: Optional[str] = None
|
||||
client_secret_file: Optional[str] = None
|
||||
scopes: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppConfig:
|
||||
"""App link configuration."""
|
||||
|
||||
url: str
|
||||
name: Optional[str] = None
|
||||
image: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
groups: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""Top-level configuration."""
|
||||
|
||||
app: CoreConfig
|
||||
oidc: OidcConfig
|
||||
apps: Dict[str, AppConfig] = field(default_factory=dict)
|
||||
|
||||
|
||||
class ConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _validate(config: Config) -> None:
|
||||
oidc = config.oidc
|
||||
if oidc.client_secret is None and oidc.client_secret_file is None:
|
||||
raise ConfigError(
|
||||
"exactly one of oidc.client_secret or oidc.client_secret_file is required"
|
||||
)
|
||||
if oidc.client_secret is not None and oidc.client_secret_file is not None:
|
||||
raise ConfigError(
|
||||
"exactly one of oidc.client_secret or oidc.client_secret_file is required"
|
||||
)
|
||||
|
||||
|
||||
def load(file: str) -> Config:
|
||||
"""
|
||||
Load the configuration from a `file` path containing a TOML configuration.
|
||||
|
||||
:param file: a file path to a TOML config
|
||||
:return: the configuration object
|
||||
"""
|
||||
path = Path(file)
|
||||
cfg = dacite.from_dict(data_class=Config, data=toml.load(path))
|
||||
_validate(cfg)
|
||||
return cfg
|
||||
|
||||
|
||||
def current_config() -> Config:
|
||||
"""
|
||||
Load the configuration from the current Flask app.
|
||||
|
||||
:return: the configuration object
|
||||
"""
|
||||
return current_app.config["user_config"]
|
40
frontpage/routes.py
Normal file
40
frontpage/routes.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""Route configuration."""
|
||||
from typing import Any, Iterable, List
|
||||
|
||||
import flask
|
||||
|
||||
from flask import Blueprint, current_app, render_template
|
||||
from flask_pyoidc import OIDCAuthentication
|
||||
from flask_pyoidc.user_session import UserSession
|
||||
|
||||
from frontpage.config import AppConfig, current_config
|
||||
|
||||
|
||||
def _allowed(items_from: Iterable[Any], items_in: Iterable[Any]) -> bool:
|
||||
if len(items_from) == 0:
|
||||
return True
|
||||
for item in items_from:
|
||||
if item in items_in:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def register(auth: OIDCAuthentication, auth_provider: str) -> Blueprint:
|
||||
routes = Blueprint("routes", __name__)
|
||||
|
||||
@routes.route("/")
|
||||
@auth.oidc_auth(auth_provider)
|
||||
def home():
|
||||
"""
|
||||
Renders the home route.
|
||||
"""
|
||||
user_session = UserSession(flask.session)
|
||||
groups: List[str] = user_session.userinfo["groups"]
|
||||
|
||||
apps: AppConfig = current_config().apps
|
||||
allowed_apps = {
|
||||
ident: a for ident, a in apps.items() if _allowed(a.groups, groups)
|
||||
}
|
||||
return render_template("home.html", apps=allowed_apps, groups=groups)
|
||||
|
||||
return routes
|
34
frontpage/templates/home.html
Normal file
34
frontpage/templates/home.html
Normal file
@ -0,0 +1,34 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://unpkg.com/chota@latest">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
|
||||
<title>Front page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<div class="nav-left">
|
||||
<a class="brand" href="/">Front page</a>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container">
|
||||
<div class="row">
|
||||
{% for ident, cfg in apps.items() %}
|
||||
<div class="card col-4">
|
||||
<header>
|
||||
<a href="{{ cfg.url }}">
|
||||
{{ cfg.name or ident }}
|
||||
</a>
|
||||
</header>
|
||||
{% if cfg.image %}
|
||||
<img src="{{ cfg.image }}">
|
||||
{% endif %}
|
||||
{% if cfg.description %}
|
||||
<p>{{ cfg.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user