add tiered auto-expiry based on auth level
All checks were successful
CI / Lint & Format (push) Successful in 17s
CI / Security Scan (push) Successful in 22s
CI / Tests (push) Successful in 1m5s

This commit is contained in:
Username
2025-12-21 21:55:30 +01:00
parent 3fe631f6b9
commit e8a99d5bdd
4 changed files with 68 additions and 10 deletions

View File

@@ -816,6 +816,23 @@ class IndexView(MethodView):
burn_header = request.headers.get("X-Burn-After-Read", "").strip().lower()
burn_after_read = burn_header in ("true", "1", "yes")
# Determine default expiry based on authentication level
# Anonymous < Untrusted cert < Trusted cert (registered in PKI)
if owner is None:
# Anonymous user
default_expiry = current_app.config.get("EXPIRY_ANON", 86400)
elif trusted_client:
# Trusted certificate (registered in PKI)
from app.pki import is_trusted_certificate
if is_trusted_certificate(owner):
default_expiry = current_app.config.get("EXPIRY_TRUSTED", 2592000)
else:
default_expiry = current_app.config.get("EXPIRY_UNTRUSTED", 604800)
else:
# Has cert but not trusted
default_expiry = current_app.config.get("EXPIRY_UNTRUSTED", 604800)
expires_at = None
expiry_header = request.headers.get("X-Expiry", "").strip()
if expiry_header:
@@ -829,6 +846,10 @@ class IndexView(MethodView):
except ValueError:
pass
# Apply default expiry if none specified (0 = no expiry for trusted)
if expires_at is None and default_expiry > 0:
expires_at = int(time.time()) + default_expiry
password_hash = None
password_header = request.headers.get("X-Paste-Password", "")
if password_header:

View File

@@ -22,10 +22,17 @@ class Config:
MAX_PASTE_SIZE_AUTH = int(os.environ.get("FLASKPASTE_MAX_AUTH", 50 * 1024 * 1024)) # 50MiB
MAX_CONTENT_LENGTH = MAX_PASTE_SIZE_AUTH # Flask request limit
# Paste expiry (default 5 days)
PASTE_EXPIRY_SECONDS = int(os.environ.get("FLASKPASTE_EXPIRY", 5 * 24 * 60 * 60))
# Maximum custom expiry (default 30 days, 0 = use default expiry as max)
MAX_EXPIRY_SECONDS = int(os.environ.get("FLASKPASTE_MAX_EXPIRY", 30 * 24 * 60 * 60))
# Paste expiry (tiered by authentication level)
# Anonymous users: shortest expiry (default 1 day)
EXPIRY_ANON = int(os.environ.get("FLASKPASTE_EXPIRY_ANON", 1 * 24 * 60 * 60))
# Untrusted certs (authenticated but not registered): medium expiry (default 7 days)
EXPIRY_UNTRUSTED = int(os.environ.get("FLASKPASTE_EXPIRY_UNTRUSTED", 7 * 24 * 60 * 60))
# Trusted certs (registered in PKI): longest expiry (default 30 days, 0 = no expiry)
EXPIRY_TRUSTED = int(os.environ.get("FLASKPASTE_EXPIRY_TRUSTED", 30 * 24 * 60 * 60))
# Maximum custom expiry (default 90 days, 0 = unlimited for trusted)
MAX_EXPIRY_SECONDS = int(os.environ.get("FLASKPASTE_MAX_EXPIRY", 90 * 24 * 60 * 60))
# Legacy alias for backwards compatibility
PASTE_EXPIRY_SECONDS = EXPIRY_ANON
# Content deduplication / abuse prevention
# Throttle repeated submissions of identical content

View File

@@ -1061,6 +1061,30 @@ def is_admin_certificate(fingerprint: str) -> bool:
return bool(row and row["is_admin"])
def is_trusted_certificate(fingerprint: str) -> bool:
"""Check if a certificate is trusted (registered in PKI system).
Trusted certificates are those issued by our PKI system and still valid.
External certificates (valid for auth but not issued by us) are not trusted.
Args:
fingerprint: SHA1 fingerprint of the certificate
Returns:
True if the certificate is registered and valid in our PKI
"""
from app.database import get_db
db = get_db()
row = db.execute(
"""SELECT status FROM issued_certificates
WHERE fingerprint_sha1 = ? AND status = 'valid'""",
(fingerprint,),
).fetchone()
return row is not None
def revoke_certificate(serial: str) -> bool:
"""Revoke a certificate by serial number.

View File

@@ -178,8 +178,8 @@ class TestCustomExpiry:
data = response.get_json()
assert "expires_at" in data
def test_invalid_expiry_ignored(self, client):
"""Invalid X-Expiry values should be ignored."""
def test_invalid_expiry_uses_default(self, client):
"""Invalid X-Expiry values should use default expiry."""
for value in ["invalid", "-100", "0", ""]:
response = client.post(
"/",
@@ -188,14 +188,16 @@ class TestCustomExpiry:
)
assert response.status_code == 201
data = response.get_json()
assert "expires_at" not in data, f"Should not have expiry for: {value}"
# Default expiry is applied for anonymous users
assert "expires_at" in data, f"Should have default expiry for: {value}"
def test_paste_without_custom_expiry(self, client):
"""Paste without X-Expiry should not have expires_at."""
"""Paste without X-Expiry should use default expiry based on auth level."""
response = client.post("/", data=b"content")
assert response.status_code == 201
data = response.get_json()
assert "expires_at" not in data
# Default expiry is now applied for all users
assert "expires_at" in data
class TestExpiryCleanup:
@@ -205,7 +207,11 @@ class TestExpiryCleanup:
def app(self):
"""Create app with very short expiry for testing."""
app = create_app("testing")
app.config["PASTE_EXPIRY_SECONDS"] = 1 # 1 second default
# Set tiered expiry to 1 second for all levels
app.config["EXPIRY_ANON"] = 1
app.config["EXPIRY_UNTRUSTED"] = 1
app.config["EXPIRY_TRUSTED"] = 1
app.config["PASTE_EXPIRY_SECONDS"] = 1 # Legacy
app.config["MAX_EXPIRY_SECONDS"] = 10
return app