Django support for PostgreSQL security labels
Project description
Django Security Label
Django support for PostgreSQL security labels.
Overview
This package provides tools for applying and managing security labels in PostgreSQL databases on your Django models. This can allow you to dynamically mask specific columns to limit a user's ability to find sensitive data.
This was created with Jay Miller for the "Elephant in the Room" series. The inspiration for the project was from his blog post, "Using PostgreSQL Anonymizer to safely share data with LLMs"
Installation
pip install django-security-label
- Add the app to your
INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"django_security_label",
]
- Enable the middleware for relevant environments:
if ENVIRONMENT_VARIABLE == "staging":
MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
# MaskedReadsMiddleware must be after AuthenticationMiddleware because
# it requires a write to the database.
"django_security_label.middleware.MaskedReadsMiddleware",
]
Note: The PostgreSQL role that is used for masked reads will not have the ability to insert, update or delete data on any table that has a masking security label applied to it.
- Run the migrations. This will install the
anonprovider and configure dynamic masking. It will also create thedsl_masked_readerrole with the same permissions as your database user.
python manage.py migrate django_security_label
You can view the SQL with: python manage.py sqlmigrate django_security_label 0001
- Define your security labels on your models:
class MyModel(models.Model):
text = models.TextField()
confidential = models.TextField()
random_int = models.IntegerField()
class Meta:
# Defining any security labels will prevent any changes to the table
# when masking is enabled.
indexes = [
labels.AnonMaskSecurityLabel(
fields=["text"], mask_function=labels.MaskFunction.dummy_catchphrase
),
labels.ColumnSecurityLabel(
fields=["confidential"],
provider="anon",
string_literal="MASKED WITH VALUE $$CONFIDENTIAL$$",
),
labels.ColumnSecurityLabel(
fields=["random_int"],
provider="anon",
string_literal="MASKED WITH FUNCTION anon.random_int_between(0,50)",
),
]
- Create migrations and add a dependency on
("django_security_label", "0001)
python manage.py makemigrations
The dependency on ("django_security_label", "0001_initial") will ensure that your security labels will be applied after the anon provider is installed.
Example migration file:
from django.db import migrations, models
import django_security_label.labels
class Migration(migrations.Migration):
initial = True
dependencies = [
("django_security_label", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="MyModel",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("text", models.TextField()),
("confidential", models.TextField()),
("random_int", models.IntegerField()),
],
options={
"indexes": [
django_security_label.labels.AnonMaskSecurityLabel(
fields=["text"],
mask_function=django_security_label.labels.MaskFunction[
"dummy_catchphrase"
],
name="mymodel_text_6adba0_idx",
provider="anon",
string_literal="MASKED WITH FUNCTION anon.dummy_catchphrase()",
),
django_security_label.labels.ColumnSecurityLabel(
fields=["confidential"],
name="mymodel_confide_030817_idx",
provider="anon",
string_literal="MASKED WITH VALUE $$CONFIDENTIAL$$",
),
django_security_label.labels.ColumnSecurityLabel(
fields=["random_int"],
name="mymodel_random__45b12e_idx",
provider="anon",
string_literal="MASKED WITH FUNCTION anon.random_int_between(0,50)",
),
],
},
),
]
Documentation
Examples
Below are examples of the Django admin using the masked reading with a superuser and a typical staff user.
Superuser / unmasked Read
Staff user / masked Read
Controlling masked reads
The default configuration of this package uses dynamic masking. This is because static masking will irrevocably destroy your database's data. While this is a valuable tool for some staging environments, it's not the goal at the moment.
The challenge is to use a different database role when we need to have masked reads. This is why we create the dsl_masked_reader role. We can switch to this role with SET SESSION ROLE dsl_masked_reader; and ROLE RESET; to enable and disable masked reads respectively.
The middleware, MaskedReadsMiddleware controls when the role is switched. It does so crudely at this point by only allowing user.is_superuser users to use the default database role. All other queries will use the dsl_masked_reader role and be subject to the security labels that were defined on the columns.
Customizing when masking is used
Please copy and update the code in MaskedReadsMiddleware to suit your needs. The majority of the complexity will be in your definition of use_masked_reads.
For example, if you only wanted to force anonymous users to have masked reads:
from __future__ import annotations
from django.db import connection
from django.http import HttpRequest
from django_security_label.middleware import enable_masked_reads, disable_masked_reads
def use_masked_reads(request: HttpRequest) -> bool:
user = getattr(request, "user", None)
return user is None or not user.is_authenticated
class AnonymousOnlyMaskedReadsMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if enable_masking := use_masked_reads(request):
enable_masked_reads()
response = self.get_response(request)
if enable_masking:
disable_masked_reads()
return response
Anonymizer functions
The PostgreSQL Anonymizer provider includes dozens of functions.
You can use a predefined function such as fake_email the following:
from django_security_label import labels
labels.AnonMaskSecurityLabel(
fields=["email"], mask_function=labels.MaskFunction.fake_email
)
You can also define the string literal portion of the SECURITY LABEL with the following:
labels.ColumnSecurityLabel(
fields=["confidential"],
provider="anon",
string_literal="MASKED WITH VALUE $$CONFIDENTIAL$$",
),
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_security_label-0.1.0.tar.gz.
File metadata
- Download URL: django_security_label-0.1.0.tar.gz
- Upload date:
- Size: 12.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b3c3772978020807ea0499345fe75f570b9491145f3c22ca60b31b8d61a245c5
|
|
| MD5 |
fd0b5f60cf7735e1724686b70e6b6335
|
|
| BLAKE2b-256 |
53a2bc8b7d4afdfedad0f56d7f5b1abb40c7928b8cdb43e2fff5bf4f462fa016
|
Provenance
The following attestation bundles were made for django_security_label-0.1.0.tar.gz:
Publisher:
release.yml on tim-schilling/django-security-label
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_security_label-0.1.0.tar.gz -
Subject digest:
b3c3772978020807ea0499345fe75f570b9491145f3c22ca60b31b8d61a245c5 - Sigstore transparency entry: 857393264
- Sigstore integration time:
-
Permalink:
tim-schilling/django-security-label@d68a08f0a6cefaff42a0bc0c994a0e28b4f6efa7 -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/tim-schilling
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d68a08f0a6cefaff42a0bc0c994a0e28b4f6efa7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_security_label-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_security_label-0.1.0-py3-none-any.whl
- Upload date:
- Size: 8.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
03816a13be6b4c038c4302e2cec5c263f3a642d3b37754605c5dc3d5b1770a2c
|
|
| MD5 |
124c8e9ba8711a89caa810bef6197f2d
|
|
| BLAKE2b-256 |
31eb0ed41d1038e169854c458fe5e6e66be56d47aafd0725e70863c40ba09b6d
|
Provenance
The following attestation bundles were made for django_security_label-0.1.0-py3-none-any.whl:
Publisher:
release.yml on tim-schilling/django-security-label
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_security_label-0.1.0-py3-none-any.whl -
Subject digest:
03816a13be6b4c038c4302e2cec5c263f3a642d3b37754605c5dc3d5b1770a2c - Sigstore transparency entry: 857393285
- Sigstore integration time:
-
Permalink:
tim-schilling/django-security-label@d68a08f0a6cefaff42a0bc0c994a0e28b4f6efa7 -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/tim-schilling
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d68a08f0a6cefaff42a0bc0c994a0e28b4f6efa7 -
Trigger Event:
push
-
Statement type: