Implemented OIDC für pixelfed

This commit is contained in:
Kevin Veen-Birkenbach 2025-06-19 10:02:03 +02:00
parent ceab517dfa
commit 03192dd4f3
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
9 changed files with 170 additions and 30 deletions

View File

@ -6,6 +6,8 @@ from typing import Dict
from utils.handler.yaml import YamlHandler from utils.handler.yaml import YamlHandler
from utils.handler.vault import VaultHandler, VaultScalar from utils.handler.vault import VaultHandler, VaultScalar
import string import string
import sys
import base64
class InventoryManager: class InventoryManager:
def __init__(self, role_path: Path, inventory_path: Path, vault_pw: str, overrides: Dict[str, str]): 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() return bcrypt.hashpw(pw, bcrypt.gensalt()).decode()
if algorithm == "alphanumeric": if algorithm == "alphanumeric":
return self.generate_secure_alphanumeric(64) return self.generate_secure_alphanumeric(64)
if algorithm == "base64_prefixed_32":
return "base64:" + base64.b64encode(secrets.token_bytes(32)).decode()
return "undefined" return "undefined"

View File

@ -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. - **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. - **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. - **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 ## Additional Resources

View File

@ -2,30 +2,22 @@
## Description ## Description
Pixelfed is a decentralized image sharing platform that champions creativity and privacy. It offers a secure, communitydriven 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 ## 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:** * **Decentralized Content Sharing:** Empower users to share photos and visual content across an interoperable, federated network with enhanced privacy controls.
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 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.
- **Modern, Responsive Web Interface:** * **Flexible Configuration:** Customize cache sizes, domain settings, and authentication options via environment variables and templated configuration files.
Access an intuitive and dynamic user interface designed for effortless browsing, administration, and content management. * **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.
- **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 webbased management tools to clear cache, manage the database, and monitor application status.
## Other Resources ## Other Resources
- [Pixelfed GitHub Repository](https://github.com/pixelfed/pixelfed) * [Official Pixelfed website](https://pixelfed.org/)
- [OIDC Plugin Installation Guide](https://chat.openai.com/share/67a4f448-4be8-800f-8639-4c15cb2fb44e) * [Pixelfed GitHub repository](https://github.com/pixelfed/pixelfed)

View File

@ -1,2 +0,0 @@
# Todo
- [Integrate OIDC as soon as possible](https://github.com/pixelfed/pixelfed/pull/5608)

View File

@ -1,5 +1,5 @@
credentials: credentials:
app_key: app_key:
description: "Application key used for encryption in Pixelfed (.env APP_KEY)" description: "Generic 32-byte base64 key with base64: prefix"
algorithm: "plain" algorithm: base64_prefixed_32
validation: "^base64:[A-Za-z0-9+/=]{40,}$" validation: '^base64:[A-Za-z0-9+/]{43}=$'

View File

@ -149,6 +149,6 @@ PF_OIDC_USERNAME_FIELD="{{oidc.attributes.username}}"
PF_OIDC_FIELD_ID="{{oidc.attributes.username}}" PF_OIDC_FIELD_ID="{{oidc.attributes.username}}"
PF_OIDC_CLIENT_SECRET={{oidc.client.secret}} PF_OIDC_CLIENT_SECRET={{oidc.client.secret}}
PF_OIDC_CLIENT_ID={{oidc.client.id}} PF_OIDC_CLIENT_ID={{oidc.client.id}}
PF_OIDC_SCOPES="openid,profile,email" PF_OIDC_SCOPES="openid profile email"
{% endif %} {% endif %}

View File

@ -1,16 +1,18 @@
titel: "Pictures on {{primary_domain}}" titel: "Pictures on {{primary_domain}}"
#version: "latest" #version: "latest"
images: images:
pixelfed: "ghcr.io/pixelfed/pixelfed:latest" pixelfed: "zknt/pixelfed:latest"
features: features:
matomo: true matomo: true
css: true css: false # Needs to be reactivated
portfolio_iframe: false portfolio_iframe: false
central_database: true central_database: true
oidc: true
csp: csp:
flags: flags:
script-src: script-src:
unsafe-eval: true unsafe-eval: true
unsafe-inline: true
script-src-elem: script-src-elem:
unsafe-inline: true unsafe-inline: true
unsafe-eval: true unsafe-eval: true

View File

@ -1,5 +1,5 @@
credentials: credentials:
app_key: app_key:
description: "Application encryption key for Snipe-IT (.env APP_KEY)" description: "Generic 32-byte base64 key with base64: prefix"
algorithm: "plain" algorithm: base64_prefixed_32
validation: "^base64:[A-Za-z0-9+/=]{40,}$" validation: '^base64:[A-Za-z0-9+/]{43}=$'

View File

@ -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": {}},
)