Compare commits
7 Commits
6969252a9c
...
trunk
Author | SHA1 | Date | |
---|---|---|---|
57f4ff5187
|
|||
7d59e09968
|
|||
4537511dab
|
|||
74051f3afc
|
|||
c3831e9efe
|
|||
860dbe6032
|
|||
aa86f5de48
|
37
README.md
Normal file
37
README.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# frontpage (name pending)
|
||||||
|
|
||||||
|
The front page of your self-hosted server.
|
||||||
|
|
||||||
|
This app fits the use case of having multiple applications with access gated by
|
||||||
|
an OIDC provider, and showing a user what applications they have access to.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
frontpage -c CONFIG.TOML
|
||||||
|
```
|
||||||
|
|
||||||
|
where a minimal config file looks like:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[oidc]
|
||||||
|
client_id = "some_id"
|
||||||
|
client_secret = "some_secret"
|
||||||
|
issuer = "https://auth.example.com/oauth"
|
||||||
|
scopes = [ "groups" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Applications are defined using the `apps` keys:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[apps.login]
|
||||||
|
name = "Login portal"
|
||||||
|
url = "https://auth.example.com"
|
||||||
|
description = "Update your user details"
|
||||||
|
groups = [ "users" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, only users whose OIDC groups claim includes `users` will be
|
||||||
|
allowed to see a link to the login portal. Protection of the link, should a user
|
||||||
|
gain access to it otherwise, is expected to be done externall (e.g., via an
|
||||||
|
ingress controller).
|
67
flake.lock
generated
67
flake.lock
generated
@ -1,12 +1,15 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1659877975,
|
"lastModified": 1685518550,
|
||||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -16,12 +19,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-utils_2": {
|
"flake-utils_2": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems_2"
|
||||||
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1659877975,
|
"lastModified": 1685518550,
|
||||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -32,26 +38,27 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1662166109,
|
"lastModified": 1685533922,
|
||||||
"narHash": "sha256-cmNWUeVDfSJC9y8nmX2O/7kuOXJU1ZVFJMYP87qrm/Y=",
|
"narHash": "sha256-y4FCQpYafMQ42l1V+NUrMel9RtFtZo59PzdzflKR/lo=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "cb5a1a003dde9c16a1ae4b28cbe7bf0fab15da32",
|
"rev": "3a70dd92993182f8e514700ccf5b1ae9fc8a3b8d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-23.05",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1662166109,
|
"lastModified": 1685580370,
|
||||||
"narHash": "sha256-cmNWUeVDfSJC9y8nmX2O/7kuOXJU1ZVFJMYP87qrm/Y=",
|
"narHash": "sha256-zTPVdZwLVQl/y0QTZEtYs9iNvZW6H9h+/MZsKdUinu8=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "cb5a1a003dde9c16a1ae4b28cbe7bf0fab15da32",
|
"rev": "fabe2064486b607c2516296ce6108549de0649c4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -66,11 +73,11 @@
|
|||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1662044036,
|
"lastModified": 1685418143,
|
||||||
"narHash": "sha256-+5YZPznhy1gEKPdWiZj7UcLoRaLbfvUDr8OzOY+75jM=",
|
"narHash": "sha256-q2ORekI8au0pGMtOLQI8WMCJBxjzWgYRHpiEOVSBq3w=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "poetry2nix",
|
"repo": "poetry2nix",
|
||||||
"rev": "efe5b281b51c22495c488480d23d7bb1426bf3ba",
|
"rev": "f11cc14e28078c701072f2d1fb34a6495c9376b1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -85,6 +92,36 @@
|
|||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"poetry2nix": "poetry2nix"
|
"poetry2nix": "poetry2nix"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
35
flake.nix
35
flake.nix
@ -2,32 +2,53 @@
|
|||||||
description = "Application packaged using poetry2nix";
|
description = "Application packaged using poetry2nix";
|
||||||
|
|
||||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs";
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05";
|
||||||
inputs.poetry2nix.url = "github:nix-community/poetry2nix";
|
inputs.poetry2nix.url = "github:nix-community/poetry2nix";
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils, poetry2nix }: {
|
outputs = { self, nixpkgs, flake-utils, poetry2nix }: {
|
||||||
overlay = nixpkgs.lib.composeManyExtensions [
|
overlay = nixpkgs.lib.composeManyExtensions [
|
||||||
poetry2nix.overlay
|
poetry2nix.overlay
|
||||||
(final: prev: {
|
(final: prev: {
|
||||||
frontpage = prev.poetry2nix.mkPoetryApplication {
|
frontpage = prev.poetry2nix.mkPoetryApplication
|
||||||
python = prev.python39;
|
{
|
||||||
projectDir = ./.;
|
python = prev.python39;
|
||||||
};
|
projectDir = ./.;
|
||||||
|
overrides =
|
||||||
|
prev.poetry2nix.overrides.withDefaults (final_: prev_:
|
||||||
|
nixpkgs.lib.listToAttrs
|
||||||
|
(builtins.map
|
||||||
|
(name: {
|
||||||
|
inherit name;
|
||||||
|
value = prev_."${name}".overridePythonAttrs (old: {
|
||||||
|
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ final_.setuptools ];
|
||||||
|
});
|
||||||
|
})
|
||||||
|
[ "beaker" "flask-pyoidc-oda" "oic" ]));
|
||||||
|
};
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
|
nixosModules = rec {
|
||||||
|
default = frontpage;
|
||||||
|
frontpage = import ./nixos;
|
||||||
|
};
|
||||||
} // (flake-utils.lib.eachDefaultSystem (system:
|
} // (flake-utils.lib.eachDefaultSystem (system:
|
||||||
let pkgs = import nixpkgs { inherit system; overlays = [ self.overlay ]; }; in
|
let pkgs = import nixpkgs { inherit system; overlays = [ self.overlay ]; }; in
|
||||||
{
|
{
|
||||||
apps = {
|
apps = {
|
||||||
|
default = pkgs.frontpage;
|
||||||
frontpage = pkgs.frontpage;
|
frontpage = pkgs.frontpage;
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultApp = pkgs.frontpage;
|
|
||||||
|
|
||||||
devShell = (pkgs.poetry2nix.mkPoetryEnv {
|
devShell = (pkgs.poetry2nix.mkPoetryEnv {
|
||||||
python = pkgs.python39;
|
python = pkgs.python39;
|
||||||
projectDir = ./.;
|
projectDir = ./.;
|
||||||
extraPackages = ps: [ ps.python-lsp-server ];
|
extraPackages = ps: [ ps.python-lsp-server ];
|
||||||
}).env;
|
}).env;
|
||||||
|
|
||||||
|
packages = {
|
||||||
|
default = pkgs.frontpage;
|
||||||
|
frontpage = pkgs.frontpage;
|
||||||
|
};
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ class CoreConfig:
|
|||||||
|
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
port: int = 5000
|
port: int = 5000
|
||||||
|
name: str = "Front page"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -43,8 +44,8 @@ class AppConfig:
|
|||||||
class Config:
|
class Config:
|
||||||
"""Top-level configuration."""
|
"""Top-level configuration."""
|
||||||
|
|
||||||
core: CoreConfig
|
core: CoreConfig = field(default_factory=CoreConfig)
|
||||||
oidc: OidcConfig
|
oidc: OidcConfig = field(default_factory=OidcConfig)
|
||||||
apps: Dict[str, AppConfig] = field(default_factory=dict)
|
apps: Dict[str, AppConfig] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ from flask import Blueprint, current_app, render_template
|
|||||||
from flask_pyoidc import OIDCAuthentication
|
from flask_pyoidc import OIDCAuthentication
|
||||||
from flask_pyoidc.user_session import UserSession
|
from flask_pyoidc.user_session import UserSession
|
||||||
|
|
||||||
from frontpage.config import AppConfig, current_config
|
from frontpage.config import AppConfig, Config, current_config
|
||||||
|
|
||||||
|
|
||||||
def _allowed(items_from: Iterable[Any], items_in: Iterable[Any]) -> bool:
|
def _allowed(items_from: Iterable[Any], items_in: Iterable[Any]) -> bool:
|
||||||
@ -29,12 +29,16 @@ def register(auth: OIDCAuthentication, auth_provider: str) -> Blueprint:
|
|||||||
Renders the home route.
|
Renders the home route.
|
||||||
"""
|
"""
|
||||||
user_session = UserSession(flask.session)
|
user_session = UserSession(flask.session)
|
||||||
groups: List[str] = user_session.userinfo["groups"]
|
groups: List[str] = user_session.userinfo.get("groups") or []
|
||||||
|
|
||||||
apps: AppConfig = current_config().apps
|
config: Config = current_config()
|
||||||
|
name = config.core.name
|
||||||
|
apps = config.apps
|
||||||
allowed_apps = {
|
allowed_apps = {
|
||||||
ident: a for ident, a in apps.items() if _allowed(a.groups, groups)
|
ident: a for ident, a in apps.items() if _allowed(a.groups, groups)
|
||||||
}
|
}
|
||||||
return render_template("home.html", apps=allowed_apps, groups=groups)
|
return render_template(
|
||||||
|
"home.html", brand_name=name, apps=allowed_apps, groups=groups
|
||||||
|
)
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<div class="nav-left">
|
<div class="nav-left">
|
||||||
<a class="brand" href="/">Front page</a>
|
<a class="brand" href="/">{{ brand_name }}</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main class="container">
|
<main class="container">
|
||||||
|
76
nixos/default.nix
Normal file
76
nixos/default.nix
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
cfg = config.services.frontpage;
|
||||||
|
|
||||||
|
toml = pkgs.formats.toml { };
|
||||||
|
fullSettings = recursiveUpdate cfg.settings {
|
||||||
|
core.port = cfg.port;
|
||||||
|
oidc.client_secret = "@SECRET@";
|
||||||
|
};
|
||||||
|
settingsFile = toml.generate "config.toml" fullSettings;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.frontpage = {
|
||||||
|
enable = mkEnableOption "frontpage";
|
||||||
|
|
||||||
|
package = mkPackageOption pkgs "frontpage" { };
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "frontpage";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "frontpage";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 32195;
|
||||||
|
};
|
||||||
|
|
||||||
|
oidcSecretFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = ''
|
||||||
|
Path to a file containing the OIDC secret for the application.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = mkOption {
|
||||||
|
type = with types; attrsOf anything;
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Settings attribute set as described by the documentation.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
systemd.services.frontpage = {
|
||||||
|
description = "Web front page";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
preStart = ''
|
||||||
|
sed \
|
||||||
|
"s=@SECRET@=$(<${cfg.oidcSecretFile})=" \
|
||||||
|
${settingsFile} \
|
||||||
|
> /run/frontpage/config.toml
|
||||||
|
'';
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "2s";
|
||||||
|
ExecStart = "${cfg.package}/bin/frontpage -c /run/frontpage/config.toml";
|
||||||
|
RuntimeDirectory = [ "frontpage" ];
|
||||||
|
User = cfg.user;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users."${cfg.user}" = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups."${cfg.group}" = { };
|
||||||
|
};
|
||||||
|
}
|
Reference in New Issue
Block a user