Harden security

This commit is contained in:
2026-03-05 05:42:19 +00:00
parent 06735bdfb1
commit 438e561da0
75 changed files with 6260 additions and 278 deletions

393
vendor/django-crud-mixins/README.md vendored Normal file
View File

@@ -0,0 +1,393 @@
# 🚀 django-crud-mixins
**Reusable Django CRUD mixins** for rapid development of single-page applications (SPA) and grid-based UIs.
Designed for **HTMX**, **Gridstack.js**, and **Django ORM** with built-in **access control** and **permissions**.
## 📌 Features
- **Django 4+ support** with modular mixins
- **HTMX-friendly partials**
- **Pre-configured Gridstack.js widget support**
- **RBAC enforcement with per-user filtering**
- **Reusable mixins for List, Create, Read, Update, Delete**
- **Automatic permission enforcement on querysets**
- **Seamless integration with Django Forms and QuerySets**
- **Prevents unauthorized data access**
- **Extra Buttons for dynamic action-based UI extensions**
- **Context-aware detail views with Pretty Print formatting**
## 📑 CRUD Mixins Overview
| Mixin | Description |
|----------------------|--------------------------------------------------|
| `ObjectList` | List view with pagination and permission-based filtering |
| `ObjectCreate` | Create new objects with form validation |
| `ObjectRead` | Read-only detail view with pre-fetching |
| `ObjectUpdate` | Edit existing objects with permission enforcement |
| `ObjectDelete` | Soft-delete or hard-delete objects securely |
| `RestrictedViewMixin` | Enforces user-based filtering on querysets |
| `RestrictedFormMixin` | Auto-filters form choices based on user permissions |
---
## ⚙️ Installation
Add to your `requirements.txt`:
```shell
git+https://git.zm.is/XF/django-crud-mixins
```
Or install via pip:
```shell
pip install git+https://git.zm.is/XF/django-crud-mixins
```
## 🔧 Usage
### 📂 Import the CRUD mixins
```python
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectRead, ObjectUpdate
```
### 📝 Define CRUD Views
```python
class AccountList(LoginRequiredMixin, ObjectList):
list_template = "partials/account-list.html"
model = Account
page_title = "List of accounts"
list_url_name = "accounts"
list_url_args = ["type"]
submit_url_name = "account_create"
class AccountCreate(LoginRequiredMixin, ObjectCreate):
model = Account
form_class = AccountForm
submit_url_name = "account_create"
class AccountUpdate(LoginRequiredMixin, ObjectUpdate):
model = Account
form_class = AccountForm
submit_url_name = "account_update"
class AccountDelete(LoginRequiredMixin, ObjectDelete):
model = Account
```
### Add to `urls.py`
```python
path("accounts/<str:type>/", accounts.AccountList.as_view(), name="accounts"),
path(
"accounts/<str:type>/create/",
accounts.AccountCreate.as_view(),
name="account_create",
),
path(
"accounts/<str:type>/update/<str:pk>/",
accounts.AccountUpdate.as_view(),
name="account_update",
),
path(
"accounts/<str:type>/delete/<str:pk>/",
accounts.AccountDelete.as_view(),
name="account_delete",
),
```
## ⚙️ Configuration Options
### General Options
| Variable | Description |
|-----------------|----------------------------------|
| `list_template` | Template name for list view |
| `model` | Django model used for the view |
| `page_title` | Title of the page |
| `page_subtitle` | Subtitle of the page |
### List URLs
| Variable | Description |
|----------------|----------------------------------|
| `list_url_name` | URL name for listing objects |
| `list_url_args` | URL arguments for list endpoint |
### Submit URLs (Forms)
| Variable | Description |
|----------------|------------------------------|
| `submit_url_name` | URL for submitting forms |
| `submit_url_args` | URL args for submitting |
### ObjectList-Specific Options
| Variable | Description |
|----------------------|------------------------------|
| `delete_all_url_name` | URL for bulk deletion |
| `widget_options` | Gridstack widget config |
### ObjectCreate & ObjectUpdate
| Variable | Description |
|--------------|----------------------------------|
| `hide_cancel` | Hide cancel button in forms |
### ObjectUpdate-Only
| Variable | Description |
|--------------|-----------------------------------|
| `pk_required` | Enforce primary key requirement |
### 🔄 Extra Buttons
The mixins allow dynamic **extra buttons** to be added to views for custom actions.
Extra buttons are rendered inside the `<div class="buttons">` section of each view:
```html
{% for button in extra_buttons %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-{{ button.method }}="{{ button.url }}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
{% if button.confirm %}hx-confirm="Are you sure you wish to {{ button.action }}?"{% endif %}
class="button">
<span class="icon-text">
<span class="icon">
<i class="{{ button.icon }}"></i>
</span>
<span>{{ button.label }}</span>
</span>
</button>
{% endfor %}
```
#### Structure of Extra Buttons
Each extra button is a dictionary with:
| Key | Description |
|-----------|----------------------------------------------|
| `method` | HTMX method (`get`, `post`, `delete`, etc.) |
| `url` | Endpoint to be triggered |
| `action` | Action name (for confirmation) |
| `icon` | CSS class for icon (FontAwesome recommended) |
| `label` | Display text for the button |
| `confirm` | Optional confirmation prompt |
#### Example Usage in Views
```python
class CustomListView(ObjectList):
extra_buttons = [
{
"method": "post",
"url": reverse_lazy("custom-action"),
"icon": "fa-solid fa-bolt",
"label": "Trigger Action",
"confirm": True,
"action": "trigger this action"
}
]
```
### 🖼️ Pretty Print Context (pretty)
The **Pretty Print** (pretty) context is used in `ObjectRead` detail views to enhance object representation.
It is loaded via `{% load pretty %}` and ensures **formatted output of complex data**.
Implement it into your project's `templatetags` folder to use:
```python
import orjson
from django import template
register = template.Library()
@register.filter
def pretty(data):
return orjson.dumps(data, option=orjson.OPT_INDENT_2).decode("utf-8")
```
#### When to Use Pretty Print
- **Complex JSON Fields** (e.g., structured responses)
- **Nested Dictionaries** (e.g., API outputs)
- **Formatted Logs & Debugging**
## 🔥 Extending Functionality
### ✅ Enforce Query Restrictions (RBAC)
Add extra permission filters via `extra_permission_args`:
```python
class CustomListView(ObjectList):
def set_extra_args(self, user):
self.extra_permission_args = {"organization": user.organization}
```
This ensures users can only see objects from their organization.
### 📍 Hook Into Save Events
Modify or abort saves via `pre_save_mutate` and `post_save`:
```python
class SecureObjectCreate(ObjectCreate):
def pre_save_mutate(self, user, obj):
if not user.is_admin:
raise AbortSave("You do not have permission to save this object.")
def post_save(self, obj):
send_notification(f"Object {obj.id} was created.")
```
## 🔒 Secure Form QuerySets
```python
from mixins.forms import RestrictedFormMixin
from django.forms import ModelForm
class YourModelForm(RestrictedFormMixin, ModelForm):
class Meta:
model = YourModel
fields = ["name"]
```
This ensures users **only see objects they have access** to in form dropdowns.
## 🔗 Nested Objects & Relationships
### Overview
Some objects exist **within a parent object**. In this case, views and URLs must be structured to reflect this relationship. The example below demonstrates managing **Person Identifiers**, which are linked to a **Person**.
### How It Works
1. **Filtering:** The `IdentifierPermissionMixin` ensures only identifiers belonging to a specific `Person` (and owned by the user) are retrieved.
2. **View Permissions:** The `set_extra_args` method filters data by enforcing **user ownership** and **parent object constraints**.
3. **Validation:**
- `PersonIdentifierCreate` ensures an identifier cannot be added twice.
- `PersonIdentifierUpdate` restricts edits to only valid, existing identifiers.
- `PersonIdentifierDelete` removes identifiers that belong to the requesting user.
4. **URL Structure:** Each identifier action (list, create, update, delete) includes the **Person ID**, ensuring operations are always scoped to the correct entity.
### Example: Managing Person Identifiers
#### 📌 Views
| View | Description |
|------|------------|
| `PersonIdentifierList` | Displays all identifiers linked to a specific `Person`. |
| `PersonIdentifierCreate` | Adds a new identifier for a `Person`, preventing duplicates. |
| `PersonIdentifierUpdate` | Edits an existing identifier, enforcing access control. |
| `PersonIdentifierDelete` | Deletes an identifier, ensuring ownership validation. |
#### 📌 URL Routing
| Route | Description |
|-------|------------|
| `/person/<str:type>/identifiers/<str:person>/` | Lists identifiers for a `Person`. |
| `/person/<str:type>/identifiers/create/<str:person>` | Creates a new identifier under a `Person`. |
| `/person/<str:type>/identifiers/update/<str:person>/<str:pk>/` | Updates a specific identifier. |
| `/person/<str:type>/identifiers/delete/<str:person>/<str:pk>/` | Deletes an identifier for a `Person`. |
### View
```python
class IdentifierPermissionMixin:
def set_extra_args(self, user):
self.extra_permission_args = {
"person__user": user,
"person__pk": self.kwargs["person"],
}
class PersonIdentifierList(LoginRequiredMixin, IdentifierPermissionMixin, ObjectList):
list_template = "partials/identifier-list.html"
model = PersonIdentifier
page_title = "Person Identifiers"
list_url_name = "person_identifiers"
list_url_args = ["type", "person"]
submit_url_name = "person_identifier_create"
submit_url_args = ["type", "person"]
class PersonIdentifierCreate(LoginRequiredMixin, IdentifierPermissionMixin, ObjectCreate):
model = PersonIdentifier
form_class = PersonIdentifierForm
submit_url_name = "person_identifier_create"
submit_url_args = ["type", "person"]
def form_valid(self, form):
"""If the form is invalid, render the invalid form."""
try:
return super().form_valid(form)
except IntegrityError as e:
if "UNIQUE constraint failed" in str(e):
form.add_error("identifier", "Identifier rule already exists")
return self.form_invalid(form)
else:
raise e
def pre_save_mutate(self, user, obj):
try:
person = Person.objects.get(pk=self.kwargs["person"], user=user)
obj.person = person
except Person.DoesNotExist:
log.error(f"Person {self.kwargs['person']} does not exist")
raise AbortSave("person does not exist or you don't have access")
class PersonIdentifierUpdate(LoginRequiredMixin, IdentifierPermissionMixin, ObjectUpdate):
model = PersonIdentifier
form_class = PersonIdentifierForm
submit_url_name = "person_identifier_update"
submit_url_args = ["type", "pk", "person"]
class PersonIdentifierDelete(LoginRequiredMixin, IdentifierPermissionMixin, ObjectDelete):
model = PersonIdentifier
```
#### 📌 Button Example (Template)
To display a button linking to a **Persons Identifiers**:
```html
<a href="{% url 'person_identifiers' type='page' person=item.id %}">
<button class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
```
## 🚀 Example Templates
### 🔥 List Template (partials/account-list.html)
```html
<table class="table is-striped">
<thead>
<tr>
<th>Account</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for account in object_list %}
<tr>
<td>{{ account.name }}</td>
<td>{{ account.email }}</td>
<td>
<a href="{% url 'account_update' pk=account.pk %}" class="button is-small">Edit</a>
<a href="{% url 'account_delete' pk=account.pk %}" class="button is-danger is-small">Delete</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3">No accounts found.</td>
</tr>
{% endfor %}
</tbody>
</table>
```
## ⚠️ Legal Disclaimer
**django-crud-mixins is provided "as is", without warranty of any kind.**
Use responsibly and comply with local data privacy laws. The maintainers are **not responsible** for misuse.