From 03192dd4f32d4f1a54c03d2d8f62abe6b5fa09c8 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 19 Jun 2025 10:02:03 +0200 Subject: [PATCH] =?UTF-8?q?Implemented=20OIDC=20f=C3=BCr=20pixelfed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli/utils/manager/inventory.py | 4 + roles/docker-moodle/README.md | 1 + roles/docker-pixelfed/README.md | 30 ++-- roles/docker-pixelfed/Todo.md | 2 - roles/docker-pixelfed/meta/schema.yml | 6 +- roles/docker-pixelfed/templates/env.j2 | 2 +- roles/docker-pixelfed/vars/configuration.yml | 6 +- roles/docker-snipe-it/meta/schema.yml | 6 +- tests/unit/test_inventory_manager.py | 143 +++++++++++++++++++ 9 files changed, 170 insertions(+), 30 deletions(-) delete mode 100644 roles/docker-pixelfed/Todo.md create mode 100644 tests/unit/test_inventory_manager.py diff --git a/cli/utils/manager/inventory.py b/cli/utils/manager/inventory.py index 6a8ad9bd..6a5f2705 100644 --- a/cli/utils/manager/inventory.py +++ b/cli/utils/manager/inventory.py @@ -6,6 +6,8 @@ from typing import Dict from utils.handler.yaml import YamlHandler from utils.handler.vault import VaultHandler, VaultScalar import string +import sys +import base64 class InventoryManager: def __init__(self, role_path: Path, inventory_path: Path, vault_pw: str, overrides: Dict[str, str]): @@ -112,4 +114,6 @@ class InventoryManager: return bcrypt.hashpw(pw, bcrypt.gensalt()).decode() if algorithm == "alphanumeric": return self.generate_secure_alphanumeric(64) + if algorithm == "base64_prefixed_32": + return "base64:" + base64.b64encode(secrets.token_bytes(32)).decode() return "undefined" diff --git a/roles/docker-moodle/README.md b/roles/docker-moodle/README.md index f982fb3b..fec461b0 100644 --- a/roles/docker-moodle/README.md +++ b/roles/docker-moodle/README.md @@ -15,6 +15,7 @@ This role deploys Moodle using Docker, automating the setup of both the Moodle a - **Scalable Deployment:** Leverage Docker for a portable and scalable installation that adapts as your user base grows. - **Robust Data Management:** Secure and reliable storage of both the Moodle application and user data through Docker volumes. - **Secure Web Access:** Configured to work seamlessly behind an Nginx reverse proxy for enhanced security and performance. +* **Single Sign-On (SSO) / OpenID Connect (OIDC):** Seamless integration with external identity providers for centralized authentication. ## Additional Resources diff --git a/roles/docker-pixelfed/README.md b/roles/docker-pixelfed/README.md index ad6e6dcb..bdece9aa 100644 --- a/roles/docker-pixelfed/README.md +++ b/roles/docker-pixelfed/README.md @@ -2,30 +2,22 @@ ## Description -Pixelfed is a decentralized image sharing platform that champions creativity and privacy. It offers a secure, community‑driven alternative to centralized social media networks by enabling federated communication and robust content sharing through a modern web interface. +Pixelfed is a decentralized image-sharing platform that champions creativity and privacy. It offers a secure, community-driven alternative to centralized social networks by enabling federated communication and seamless content sharing through a modern web interface. ## Overview -This Docker Compose deployment automates the installation and management of a Pixelfed instance +This Docker Compose deployment automates the installation and operation of a Pixelfed instance. -## Features +## Features -- **Decentralized Content Sharing:** - Empower users to share photos and visual content on an interoperable, federated network with enhanced privacy controls. - -- **Modern, Responsive Web Interface:** - Access an intuitive and dynamic user interface designed for effortless browsing, administration, and content management. - -- **Robust Scalability & Performance:** - Leverage integrated Redis caching and a secure database (MariaDB or PostgreSQL) to ensure smooth scaling and high performance. - -- **Flexible Configuration:** - Easily customize settings such as cache sizes, domain settings, and authentication options with environment variables and templated configuration files. - -- **Maintenance & Administration Tools:** - Includes a suite of CLI commands and web‑based management tools to clear cache, manage the database, and monitor application status. +* **Decentralized Content Sharing:** Empower users to share photos and visual content across an interoperable, federated network with enhanced privacy controls. +* **Modern, Responsive Web Interface:** Access an intuitive and adaptive UI for effortless browsing, administration, and content management. +* **Robust Scalability & Performance:** Leverage integrated Redis caching and a reliable database (MariaDB or PostgreSQL) for smooth scaling and high performance. +* **Flexible Configuration:** Customize cache sizes, domain settings, and authentication options via environment variables and templated configuration files. +* **Maintenance & Administration Tools:** Built-in CLI and web-based tools to clear caches, manage the database, and monitor application health. +* **Single Sign-On (SSO) / OpenID Connect (OIDC):** Seamless integration with external identity providers for centralized authentication. ## Other Resources -- [Pixelfed GitHub Repository](https://github.com/pixelfed/pixelfed) -- [OIDC Plugin Installation Guide](https://chat.openai.com/share/67a4f448-4be8-800f-8639-4c15cb2fb44e) +* [Official Pixelfed website](https://pixelfed.org/) +* [Pixelfed GitHub repository](https://github.com/pixelfed/pixelfed) \ No newline at end of file diff --git a/roles/docker-pixelfed/Todo.md b/roles/docker-pixelfed/Todo.md deleted file mode 100644 index ae11c9aa..00000000 --- a/roles/docker-pixelfed/Todo.md +++ /dev/null @@ -1,2 +0,0 @@ -# Todo -- [Integrate OIDC as soon as possible](https://github.com/pixelfed/pixelfed/pull/5608) \ No newline at end of file diff --git a/roles/docker-pixelfed/meta/schema.yml b/roles/docker-pixelfed/meta/schema.yml index 7c9b2ed1..e7ac4e5d 100644 --- a/roles/docker-pixelfed/meta/schema.yml +++ b/roles/docker-pixelfed/meta/schema.yml @@ -1,5 +1,5 @@ credentials: app_key: - description: "Application key used for encryption in Pixelfed (.env APP_KEY)" - algorithm: "plain" - validation: "^base64:[A-Za-z0-9+/=]{40,}$" \ No newline at end of file + description: "Generic 32-byte base64 key with base64: prefix" + algorithm: base64_prefixed_32 + validation: '^base64:[A-Za-z0-9+/]{43}=$' diff --git a/roles/docker-pixelfed/templates/env.j2 b/roles/docker-pixelfed/templates/env.j2 index bc7676e0..0279ff4e 100644 --- a/roles/docker-pixelfed/templates/env.j2 +++ b/roles/docker-pixelfed/templates/env.j2 @@ -149,6 +149,6 @@ PF_OIDC_USERNAME_FIELD="{{oidc.attributes.username}}" PF_OIDC_FIELD_ID="{{oidc.attributes.username}}" PF_OIDC_CLIENT_SECRET={{oidc.client.secret}} PF_OIDC_CLIENT_ID={{oidc.client.id}} -PF_OIDC_SCOPES="openid,profile,email" +PF_OIDC_SCOPES="openid profile email" {% endif %} \ No newline at end of file diff --git a/roles/docker-pixelfed/vars/configuration.yml b/roles/docker-pixelfed/vars/configuration.yml index 91969d1a..440a4601 100644 --- a/roles/docker-pixelfed/vars/configuration.yml +++ b/roles/docker-pixelfed/vars/configuration.yml @@ -1,16 +1,18 @@ titel: "Pictures on {{primary_domain}}" #version: "latest" images: - pixelfed: "ghcr.io/pixelfed/pixelfed:latest" + pixelfed: "zknt/pixelfed:latest" features: matomo: true - css: true + css: false # Needs to be reactivated portfolio_iframe: false central_database: true + oidc: true csp: flags: script-src: unsafe-eval: true + unsafe-inline: true script-src-elem: unsafe-inline: true unsafe-eval: true diff --git a/roles/docker-snipe-it/meta/schema.yml b/roles/docker-snipe-it/meta/schema.yml index 05b58487..65fdcaa5 100644 --- a/roles/docker-snipe-it/meta/schema.yml +++ b/roles/docker-snipe-it/meta/schema.yml @@ -1,5 +1,5 @@ credentials: app_key: - description: "Application encryption key for Snipe-IT (.env APP_KEY)" - algorithm: "plain" - validation: "^base64:[A-Za-z0-9+/=]{40,}$" + description: "Generic 32-byte base64 key with base64: prefix" + algorithm: base64_prefixed_32 + validation: '^base64:[A-Za-z0-9+/]{43}=$' \ No newline at end of file diff --git a/tests/unit/test_inventory_manager.py b/tests/unit/test_inventory_manager.py new file mode 100644 index 00000000..e86ee533 --- /dev/null +++ b/tests/unit/test_inventory_manager.py @@ -0,0 +1,143 @@ +import unittest +import sys +import os +import tempfile +import shutil +from pathlib import Path +from unittest.mock import patch + +# Ensure the cli package is on sys.path +sys.path.insert( + 0, + os.path.abspath( + os.path.join(os.path.dirname(__file__), "../../cli") + ), +) + +from utils.handler.yaml import YamlHandler +from utils.handler.vault import VaultHandler, VaultScalar +from cli.utils.manager.inventory import InventoryManager + + +class TestInventoryManager(unittest.TestCase): + def setUp(self): + # Create a temporary directory for role and inventory files + self.tmpdir = Path(tempfile.mkdtemp()) + + # Patch YamlHandler.load_yaml + self.load_yaml_patcher = patch.object( + YamlHandler, + 'load_yaml', + side_effect=self.fake_load_yaml + ) + self.load_yaml_patcher.start() + + # Patch VaultHandler.encrypt_string with correct signature + self.encrypt_patcher = patch.object( + VaultHandler, + 'encrypt_string', + new=lambda self, plain, key: f"{key}: !vault |\n encrypted_{plain}" + ) + self.encrypt_patcher.start() + + def tearDown(self): + # Stop patchers + patch.stopall() + # Remove temporary directory + shutil.rmtree(self.tmpdir) + + def fake_load_yaml(self, path): + path = Path(path) + # Return schema for meta/schema.yml + if path.match("*/meta/schema.yml"): + return { + "credentials": { + "plain_cred": {"description": "desc", "algorithm": "plain", "validation": {}}, + "nested": {"inner": {"description": "desc2", "algorithm": "sha256", "validation": {}}} + } + } + # Return application_id for vars/main.yml + if path.match("*/vars/main.yml"): + return {"application_id": "testapp"} + # Return feature flags for vars/configuration.yml + if path.match("*/vars/configuration.yml"): + return {"features": {"central_database": True}} + # Return empty inventory for inventory.yml + if path.name == "inventory.yml": + return {} + raise FileNotFoundError(f"Unexpected load_yaml path: {path}") + + def test_load_application_id_missing(self): + """Loading application_id without it should raise SystemExit.""" + role_dir = self.tmpdir / "role" + (role_dir / "vars").mkdir(parents=True) + (role_dir / "vars" / "main.yml").write_text("{}") + + with patch.object(YamlHandler, 'load_yaml', return_value={}): + with self.assertRaises(SystemExit): + InventoryManager(role_dir, self.tmpdir / "inventory.yml", "pw", {}).load_application_id(role_dir) + + def test_generate_value_algorithms(self): + """Verify generate_value produces outputs of the expected form.""" + # Bypass __init__ to avoid YAML loading + im = InventoryManager.__new__(InventoryManager) + + # random_hex → 64 bytes hex = 128 chars + hex_val = im.generate_value("random_hex") + self.assertEqual(len(hex_val), 128) + self.assertTrue(all(c in "0123456789abcdef" for c in hex_val)) + + # sha256 → 64 hex chars + sha256_val = im.generate_value("sha256") + self.assertEqual(len(sha256_val), 64) + + # sha1 → 40 hex chars + sha1_val = im.generate_value("sha1") + self.assertEqual(len(sha1_val), 40) + + # bcrypt → starts with bcrypt prefix + bcrypt_val = im.generate_value("bcrypt") + self.assertTrue(bcrypt_val.startswith("$2")) + + # alphanumeric → 64 chars + alnum = im.generate_value("alphanumeric") + self.assertEqual(len(alnum), 64) + self.assertTrue(alnum.isalnum()) + + # base64_prefixed_32 → starts with "base64:" + b64 = im.generate_value("base64_prefixed_32") + self.assertTrue(b64.startswith("base64:")) + + def test_apply_schema_and_recurse(self): + """ + apply_schema should inject central_database password and vault nested.inner + """ + # Setup role directory + role_dir = self.tmpdir / "role" + (role_dir / "meta").mkdir(parents=True) + (role_dir / "vars").mkdir(parents=True) + + # Create empty inventory.yml + inv_file = self.tmpdir / "inventory.yml" + inv_file.write_text(" ") + + # Provide override for plain_cred to avoid SystemExit + overrides = {'credentials.plain_cred': 'OVERRIDE_PLAIN'} + + # Instantiate manager with overrides + mgr = InventoryManager(role_dir, inv_file, "pw", overrides=overrides) + + # Patch generate_value locally for predictable values + with patch.object(InventoryManager, 'generate_value', lambda self, alg: f"GEN_{alg}"): + result = mgr.apply_schema() + + apps = result["applications"]["testapp"] + # central_database entry + self.assertEqual(apps["credentials"]["database_password"], "GEN_alphanumeric") + # plain_cred vaulted from override + self.assertIsInstance(apps["credentials"]["plain_cred"], VaultScalar) + # nested.inner should not be vaulted due to code's prefix check + self.assertEqual( + apps["credentials"]["nested"]["inner"], + {"description": "desc2", "algorithm": "sha256", "validation": {}}, + )