git init
This commit is contained in:
1 parent
2b7e0d23c0
commit
5b649f96f2
6 files changed
+404
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,83 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Flask 2.x Proof-of-Concept to demonstrate fine-grained, dynamic RBAC and multi-tenancy using **Zitadel** as OIDC IdP. Authentication is fully delegated to Zitadel; authorization and project assignments are handled locally via SQLite.
|
||||||
|
|
||||||
|
The full specification is in [claude-context.md](claude-context.md).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Local Development (venv)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
cp .env.example .env # then fill in Zitadel credentials
|
||||||
|
flask db-init # initialize SQLite schema
|
||||||
|
flask seed-db # seed mock data (Firma Müller, 3 projects, sample notes)
|
||||||
|
flask run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker / Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build # builds image from Dockerfile, reads .env
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database CLI commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flask seed-db # wipes and re-seeds the SQLite database with test fixtures
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Flat backend, rich local authorization:**
|
||||||
|
|
||||||
|
- **Zitadel (external)** — handles login, issues OIDC tokens carrying the user's global role claim (`Admin`, `Mitarbeiter`, or `Kunde`).
|
||||||
|
- **`app/auth.py`** — Authlib OIDC client; extracts `sub`, role claims, and token scopes from the IdP response; logs token payload to stdout in development.
|
||||||
|
- **`app/models.py`** — SQLAlchemy models for the local relational layer:
|
||||||
|
- `User` (keyed on Zitadel `sub`)
|
||||||
|
- `Client` (top-level company, e.g. "Firma Müller")
|
||||||
|
- `Project` (belongs to one Client)
|
||||||
|
- `ProjectMitarbeiterMapping` (N:M: employees ↔ projects)
|
||||||
|
- `ProjectKundenMapping` (N:M: external users ↔ projects)
|
||||||
|
- `Note` (timestamped text entry tied to a Project)
|
||||||
|
- **`app/routes.py`** — all routing guards live here; unauthorized access always redirects to `login_error.html` (never raises HTTP errors).
|
||||||
|
- **`app/config.py`** — reads `.env`; follows 12-Factor style.
|
||||||
|
- **`app/database.py`** — SQLAlchemy engine/session setup.
|
||||||
|
|
||||||
|
## Role-Based Access (RBAC) Logic
|
||||||
|
|
||||||
|
| Role | Access |
|
||||||
|
|---|---|
|
||||||
|
| `Admin` | Full visibility; all clients, projects, users; global export |
|
||||||
|
| `Mitarbeiter` | Only projects linked via `ProjectMitarbeiterMapping` |
|
||||||
|
| `Kunde` | Only projects linked via `ProjectKundenMapping` for their client |
|
||||||
|
|
||||||
|
**Routing guard pattern:** Every protected route checks (1) Zitadel role from session, (2) local DB mapping for the requested resource. Any mismatch → redirect to `/error` (renders `login_error.html`). The error page has a JS back-button to support test flows.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
See `.env.example`:
|
||||||
|
|
||||||
|
```
|
||||||
|
FLASK_APP=app
|
||||||
|
FLASK_ENV=development
|
||||||
|
SECRET_KEY=...
|
||||||
|
ZITADEL_CLIENT_ID=...
|
||||||
|
ZITADEL_CLIENT_SECRET=...
|
||||||
|
ZITADEL_DISCOVERY_URL=https://<instance>.zitadel.cloud/.well-known/openid-configuration
|
||||||
|
DATABASE_URL=sqlite:///app.db
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Notes
|
||||||
|
|
||||||
|
- `Dockerfile` uses `python:3.11-slim`, runs app via `gunicorn`.
|
||||||
|
- `docker-compose.yml` builds from source (no pre-built images), mounts a volume for the SQLite file, and reads `.env` directly.
|
||||||
|
- Setting `pull_policy: always` (or equivalent) in the compose file ensures a base-image update triggers a rebuild.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{#
|
||||||
|
Datei: templates/<dateiname>.html
|
||||||
|
Autor: <Vorname Nachname> <email@beispiel.de>
|
||||||
|
Datum: YYYY-MM-DD
|
||||||
|
Zweck: <Kurze Beschreibung, welche Seite/Komponente dieses Template rendert.>
|
||||||
|
<Erwartete Jinja2-Variablen: z. B. {{ user }}, {{ projects }}, ...>
|
||||||
|
#}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}<Seitentitel>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{# Inhalt hier #}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
<Einzeiliger Titel dieses Moduls>
|
||||||
|
|
||||||
|
<Optionale Beschreibung: Was leistet dieses Modul? Welche Klassen oder
|
||||||
|
Funktionen exportiert es? Welche externen Abhängigkeiten hat es?>
|
||||||
|
|
||||||
|
:author: <Vorname Nachname> <email@beispiel.de>
|
||||||
|
:date: YYYY-MM-DD
|
||||||
|
:version: 1.0
|
||||||
|
"""
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
|
||||||
|
# System Prompt: Flask OIDC Showcase App with Zitadel (Advanced Dynamic RBAC)
|
||||||
|
|
||||||
|
## 1. Role & Objective
|
||||||
|
You are an expert Python Backend Engineer and Cloud-Native Infrastructure Specialist. Your objective is to build a complete, clean Proof of Concept (PoC) / Showcase Web Application using **Python Flask 2.x**.
|
||||||
|
|
||||||
|
The primary goal of this application is to demonstrate and validate fine-grained, dynamic Role-Based Access Control (RBAC) and multi-tenancy project assignment using **Zitadel** as an External Identity Provider (IdP) via **OIDC (Authlib)**.
|
||||||
|
|
||||||
|
The secondary goal is to show a "flat" architecture where user authentication and global identity management are completely offloaded to the IdP (Zitadel Self-Service), while complex relational authorization, project assignments, and data separation are handled locally via **SQLite**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Tech Stack & Architecture
|
||||||
|
* **Backend Framework:** Python Flask 2.x (running inside a virtual environment `venv`).
|
||||||
|
* **OIDC Library:** `Authlib` (configured to interact with Zitadel's OpenID Connect endpoints).
|
||||||
|
* **Database / ORM:** SQLite using SQLAlchemy (to manage local relational mappings).
|
||||||
|
* **Configuration:** 12-Factor-App style using environment variables via a `.env` file.
|
||||||
|
* **Deployment & Multi-Environment:** Dual-purpose Git repository structure supporting:
|
||||||
|
1. **Local Development:** Native execution via `flask run` inside `venv`.
|
||||||
|
2. **Production:** Containerized execution using a lean, production-ready Docker image (`python:3.11-slim` or equivalent) with `gunicorn`, orchestrated via `docker-compose.yml`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Directory Structure
|
||||||
|
Generate the repository using the following clean layout:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── .gitignore
|
||||||
|
├── .env.example
|
||||||
|
├── README.md
|
||||||
|
├── requirements.txt
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── Dockerfile
|
||||||
|
├── app/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── config.py
|
||||||
|
│ ├── database.py
|
||||||
|
│ ├── models.py
|
||||||
|
│ ├── auth.py
|
||||||
|
│ ├── routes.py
|
||||||
|
│ ├── templates/
|
||||||
|
│ │ ├── base.html
|
||||||
|
│ │ ├── login_error.html
|
||||||
|
│ │ ├── index.html (Dashboard with test matrix)
|
||||||
|
│ │ ├── admin.html (Global export & management)
|
||||||
|
│ │ ├── mitarbeiter.html (Project selection overview)
|
||||||
|
│ │ └── projekt.html (Project cockpit with notes log)
|
||||||
|
│ └── static/
|
||||||
|
│ └── css/
|
||||||
|
│ └── style.css
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Database Schema & Zitadel Role Mapping
|
||||||
|
|
||||||
|
The application relies on Zitadel for global identity authentication. Upon successful login, the app extracts the user's primary identity (`sub` / User ID) and their assigned global roles from the OIDC UserInfo / ID Token.
|
||||||
|
|
||||||
|
### Zitadel Global Roles (Managed in IdP):
|
||||||
|
|
||||||
|
1. **Admin:** Has global read/write privileges over all local data and management views.
|
||||||
|
2. **Mitarbeiter** (Internal Employee): Allowed to see and interact with internal projects they are assigned to.
|
||||||
|
3. **Kunde** (External Client User): Tied to a client identity; allowed to see specific project deliverables.
|
||||||
|
|
||||||
|
### SQLite Relational Schema (SQLAlchemy Models):
|
||||||
|
|
||||||
|
To support advanced multi-tenancy and dynamic project scoping, implement the following relational schema:
|
||||||
|
|
||||||
|
* **Users Table:** Stores the `zitadel_user_id` (Primary Key from OIDC token), `email`, and cached `global_role`.
|
||||||
|
* **Clients Table (Kundenstamm):** Represents the top-level company organization (e.g., "Firma Müller").
|
||||||
|
* **Projects Table (Kundenprojekte):** Represents specific project contexts belonging to a Client (e.g., "Akquise Landkreis", "Akquise Bundesstaat", "Spezielle Produktpalette"). Each project belongs to exactly **one** Client.
|
||||||
|
* **Project_Mitarbeiter_Mapping (N:M Table):** Links internal employees (`Mitarbeiter`) to multiple projects. A single project can also have multiple assigned employees.
|
||||||
|
* **Project_Kunden_Mapping (N:M Table):** Links external client users (`Kunde`) to specific projects within their parent client organization.
|
||||||
|
* **Notes Table (Korrespondenz/Notizen):** Text logs containing timestamps, an author reference (`user_id`), and a foreign key to a specific `Project`. These represent project-specific internal notes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Frontend & Routing Requirements (The Showcase Test Matrix)
|
||||||
|
|
||||||
|
### Unauthenticated / Access Denied State
|
||||||
|
|
||||||
|
* Any unauthorized access attempt, failed login, or role-check violation must redirect the user to `login_error.html`.
|
||||||
|
* **Error Page Text:** "Bitte besuch uns später wieder, du bist im Moment nicht autorisiert."
|
||||||
|
* **Crucial Feature:** This page must feature a **"Zurück" (Back) button** that programmatically returns the user to their previous browser/application context (simulating the browser's back-button history) to allow fluid end-to-end testing of authorization boundaries.
|
||||||
|
|
||||||
|
### Authenticated State (The Dashboard Matrix)
|
||||||
|
|
||||||
|
Upon successful OIDC login, the user lands on the index dashboard (`index.html`). This page must display up to **four functional testing paths** dynamically rendered based on the active user's permissions:
|
||||||
|
|
||||||
|
1. **Path: Admin Cockpit**
|
||||||
|
* *Access Rule:* Restricted strictly to users with the global Zitadel role `Admin`.
|
||||||
|
* *Features:* A master panel allowing full visibility into Clients, Projects, and User-to-Project mappings. Includes a mockup button for a **"Gesamtexport"** (Global Data Export) spanning all database tables.
|
||||||
|
|
||||||
|
|
||||||
|
2. **Path: Mitarbeiter Cockpit**
|
||||||
|
* *Access Rule:* Restricted strictly to users with the global Zitadel role `Mitarbeiter`.
|
||||||
|
* *Features:* Displays an overview listing *only* those Projects the employee is actively mapped to via the local N:M table. Clicking on a project routes them to the Project View.
|
||||||
|
|
||||||
|
|
||||||
|
3. **Path: Kundenprojekt View (Authorized Context)**
|
||||||
|
* *Access Rule:* Accessible if the logged-in user is an internal `Mitarbeiter` mapped to this project **OR** an external `Kunde` mapped to this specific project.
|
||||||
|
* *Features:* Displays project details and the associated **Notes/Korrespondenz Log**. Users can view notes and click an action to extract/export notes *strictly limited* to this project's scope.
|
||||||
|
|
||||||
|
|
||||||
|
4. **Path: Kunde außerhalb des Projekts (Failure Test Case)**
|
||||||
|
* *Access Rule:* This button simulates an attack or a misconfiguration test where an authenticated `Kunde` or `Mitarbeiter` attempts to force-navigate to a project ID/URL they are **not** mapped to in the local SQLite database.
|
||||||
|
* *Expected Behavior:* The routing guard must intercept this request, deny access, and immediately redirect to the Login Error Page.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Environment & Deployment Configuration
|
||||||
|
|
||||||
|
### `.env` File Setup
|
||||||
|
|
||||||
|
Provide an `.env.example` file formatted exactly as follows:
|
||||||
|
|
||||||
|
```env
|
||||||
|
FLASK_APP=app
|
||||||
|
FLASK_ENV=development
|
||||||
|
SECRET_KEY=replace-with-a-secure-random-key-in-prod
|
||||||
|
ZITADEL_CLIENT_ID=your-zitadel-client-id
|
||||||
|
ZITADEL_CLIENT_SECRET=your-zitadel-client-secret
|
||||||
|
ZITADEL_DISCOVERY_URL=https://<your-instance>.zitadel.cloud/.well-known/openid-configuration
|
||||||
|
DATABASE_URL=sqlite:///app.db
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Native Execution
|
||||||
|
|
||||||
|
Include instructions in the `README.md` to initialize the virtual environment natively:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
flask run
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Containerized Cloud-Native Execution
|
||||||
|
|
||||||
|
* **Dockerfile:** Create an ultra-slim, secure multi-stage or standard Docker production build using a lightweight Linux layer (e.g., `python:3.11-slim`). It must inject environment variables, resolve dependencies via `requirements.txt`, and bind the runtime application to a `gunicorn` production server.
|
||||||
|
* **docker-compose.yml:** Configure on-the-fly image building. Do **not** use pre-built registry images. It must read the `.env` file, bind ports, mount the SQLite database directory as a persistent volume if needed, and run out-of-the-box via `docker compose up --build`.
|
||||||
|
|
||||||
|
eine bedingung wäre, dass bei aktualisierung vom basis-container auch das Build erneut ausgeführt wird.
|
||||||
|
|
||||||
|
Es soll sich operativ für die Prod-Umgebung auch nativ anfühlen wie eine Prod-Umgebung.
|
||||||
|
Handhabbar, belastbar, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Developer-Experience & Validation Guidelines
|
||||||
|
|
||||||
|
1. **Real-time OIDC Logging:** Configure the Flask application to print incoming OIDC token payloads (specifically the unique user `sub`, token scopes, and claim-based roles) cleanly into standard output (`stdout`) during development. This allows real-time analysis of the Zitadel handshake.
|
||||||
|
2. **Database Seeding (`flask seed-db`):** Include a custom Flask CLI command to automate local testing. The script must initialize the SQLite schema and pre-populate mock data:
|
||||||
|
* At least 1 Client record ("Firma Müller").
|
||||||
|
* At least 3 separate Project records under "Firma Müller" (e.g., "Akquise Landkreis", "Akquise Bundesstaat").
|
||||||
|
* Mock rows in the N:M tables linking explicit test user IDs to specific projects.
|
||||||
|
* Sample records in the Notes table attached to those projects.
|
||||||
|
|
||||||
|
|
||||||
|
3. **Clean Code Focus:** Keep CSS and page layouts minimal, clean, and unbloated. The engineering focus must remain entirely on the strictness of the routing guards, database queries, and the security handshake.
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# Strukturelle Vorgaben – Quellcode-Konventionen
|
||||||
|
|
||||||
|
Dieses Dokument legt verbindliche Konventionen für alle Quelldateien im Projekt fest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Datei-Header
|
||||||
|
|
||||||
|
Jede Quelldatei beginnt mit einem standardisierten Header. Der Header dient gleichzeitig als Modul-Docstring und wird von **Sphinx** zur automatischen Dokumentationserzeugung ausgewertet (Python-Äquivalent zu JavaDoc).
|
||||||
|
|
||||||
|
### Python (`.py`)
|
||||||
|
|
||||||
|
Der Header ist ein Modul-Docstring (triple-quoted) direkt am Dateianfang. Sphinx liest ihn automatisch aus:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
<Einzeiliger Titel des Moduls>
|
||||||
|
|
||||||
|
<Optionale längere Beschreibung, was dieses Modul leistet, welche
|
||||||
|
Klassen/Funktionen es exportiert und welche Abhängigkeiten es hat.>
|
||||||
|
|
||||||
|
:author: Vorname Nachname <email@beispiel.de>
|
||||||
|
:date: YYYY-MM-DD
|
||||||
|
:version: 1.0
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
Pflichtfelder: `:author:`, `:date:`
|
||||||
|
Optionale Felder: `:version:`, `:see:`, `:note:`
|
||||||
|
|
||||||
|
### Jinja2 / HTML (`.html`)
|
||||||
|
|
||||||
|
Jinja2-Kommentare (`{# ... #}`) werden vom Browser nicht gerendert und sind deshalb die richtige Wahl für Template-Header:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{#
|
||||||
|
Datei: templates/beispiel.html
|
||||||
|
Autor: Vorname Nachname <email@beispiel.de>
|
||||||
|
Datum: YYYY-MM-DD
|
||||||
|
Zweck: Kurze Beschreibung, welche Seite/Komponente dieses Template rendert
|
||||||
|
und welche Jinja2-Variablen es erwartet.
|
||||||
|
#}
|
||||||
|
```
|
||||||
|
|
||||||
|
Pflichtfelder: `Datei:`, `Autor:`, `Datum:`, `Zweck:`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Dokumentationserzeugung mit Sphinx
|
||||||
|
|
||||||
|
Python bietet mit **Sphinx** ein mächtiges Werkzeug analog zu JavaDoc. Es liest Modul-Docstrings (inkl. `:author:`/`:date:`-Felder) und erzeugt daraus eine vollständige HTML-Dokumentation.
|
||||||
|
|
||||||
|
### Einrichtung (einmalig)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install sphinx
|
||||||
|
cd docs/
|
||||||
|
sphinx-quickstart # Interaktiver Setup-Assistent
|
||||||
|
```
|
||||||
|
|
||||||
|
Im Setup-Assistenten:
|
||||||
|
- `autodoc` aktivieren (liest Docstrings automatisch)
|
||||||
|
- Projektname, Autor und Version eingeben
|
||||||
|
|
||||||
|
Anschließend in `docs/conf.py` sicherstellen:
|
||||||
|
|
||||||
|
```python
|
||||||
|
extensions = [
|
||||||
|
"sphinx.ext.autodoc",
|
||||||
|
"sphinx.ext.viewcode", # Links zum Quellcode
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dokumentation aus dem `app/`-Verzeichnis einlesen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sphinx-apidoc -o docs/source/ app/
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieser Befehl scannt alle `.py`-Dateien unter `app/` und erzeugt `.rst`-Stub-Dateien, die den Modul-Docstring und alle Klassen/Funktionen einbinden.
|
||||||
|
|
||||||
|
### Dokumentation bauen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd docs/
|
||||||
|
make html
|
||||||
|
# Ergebnis liegt unter: docs/_build/html/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ablauf
|
||||||
|
|
||||||
|
```
|
||||||
|
Modul-Docstrings → sphinx-apidoc → .rst-Dateien → make html → HTML-Doku
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Hinweis:** Für HTML-Templates gibt es kein Python-natives Doku-Tool. Die Header-Kommentare dienen dort als Inline-Dokumentation für Entwickler.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Template-Dateien
|
||||||
|
|
||||||
|
Im Verzeichnis [`_templates/`](_templates/) liegen Vorlagen, von denen sich der Entwickler **jederzeit eine frische Kopie ziehen** kann:
|
||||||
|
|
||||||
|
| Template | Verwendung |
|
||||||
|
|---|---|
|
||||||
|
| [`_templates/_template.py`](_templates/_template.py) | Neue Python-Quelldatei |
|
||||||
|
| [`_templates/_template.html`](_templates/_template.html) | Neues Jinja2/HTML-Template |
|
||||||
|
|
||||||
|
### Workflow: neue Datei aus Template erzeugen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python-Datei
|
||||||
|
cp _templates/_template.py app/mein_modul.py
|
||||||
|
|
||||||
|
# HTML-Template
|
||||||
|
cp _templates/_template.html app/templates/meine_seite.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Anschließend im kopierten File die Platzhalter (`<...>`) ersetzen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Kurzreferenz – Pflichtfelder auf einen Blick
|
||||||
|
|
||||||
|
| Feldname | Python (Docstring) | HTML (Kommentar) | Pflicht |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Autor | `:author:` | `Autor:` | Ja |
|
||||||
|
| Datum | `:date:` | `Datum:` | Ja |
|
||||||
|
| Zweck/Titel | Erste Docstring-Zeile | `Zweck:` | Ja |
|
||||||
|
| Version | `:version:` | – | Nein |
|
||||||
|
| Dateiname | – | `Datei:` | Ja |
|
||||||
Reference in new issue
Block a user