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