Compare commits

..

7 Commits

Author SHA1 Message Date
semantic-release-bot 7a02d25d0b chore(release): 8.1.2 [skip ci]
## [8.1.2](https://github.com/mediacms-io/mediacms/compare/v8.1.1...v8.1.2) (2026-05-18)

### Bug Fixes

* remove redundant check ([#1528](https://github.com/mediacms-io/mediacms/issues/1528)) ([c7a673b](https://github.com/mediacms-io/mediacms/commit/c7a673bbbf46efc37621dc4a5109a85fc10e1317))
2026-05-18 11:45:18 +00:00
Markos Gogoulos c7a673bbbf fix: remove redundant check (#1528) 2026-05-18 14:44:40 +03:00
semantic-release-bot b0c0d9a83f chore(release): 8.1.1 [skip ci]
## [8.1.1](https://github.com/mediacms-io/mediacms/compare/v8.1.0...v8.1.1) (2026-05-18)

### Bug Fixes

* x-accell headers on uploaded poster ([#1526](https://github.com/mediacms-io/mediacms/issues/1526)) ([ae63a5a](https://github.com/mediacms-io/mediacms/commit/ae63a5af647c8865b96e6e50dda1ea9d29b5bd0b))
2026-05-18 11:25:57 +00:00
Markos Gogoulos ae63a5af64 fix: x-accell headers on uploaded poster (#1526) 2026-05-18 14:25:23 +03:00
semantic-release-bot 98d5d6af8b chore(release): 8.1.0 [skip ci]
## [8.1.0](https://github.com/mediacms-io/mediacms/compare/v8.0.8...v8.1.0) (2026-05-17)

### Features

* introduce x-accell headers ([9302559](https://github.com/mediacms-io/mediacms/commit/9302559d4bb3e4d0adb299ed37438b04c39e1864))
2026-05-17 07:32:43 +00:00
Markos Gogoulos 9302559d4b feat: introduce x-accell headers 2026-05-17 10:32:13 +03:00
Markos Gogoulos 279cccb980 chore(release): 8.0.8 [skip ci] 2026-05-13 21:15:15 +03:00
11 changed files with 177 additions and 21 deletions
+18
View File
@@ -1,5 +1,23 @@
# Changelog
## [8.1.2](https://github.com/mediacms-io/mediacms/compare/v8.1.1...v8.1.2) (2026-05-18)
### Bug Fixes
* remove redundant check ([#1528](https://github.com/mediacms-io/mediacms/issues/1528)) ([c7a673b](https://github.com/mediacms-io/mediacms/commit/c7a673bbbf46efc37621dc4a5109a85fc10e1317))
## [8.1.1](https://github.com/mediacms-io/mediacms/compare/v8.1.0...v8.1.1) (2026-05-18)
### Bug Fixes
* x-accell headers on uploaded poster ([#1526](https://github.com/mediacms-io/mediacms/issues/1526)) ([ae63a5a](https://github.com/mediacms-io/mediacms/commit/ae63a5af647c8865b96e6e50dda1ea9d29b5bd0b))
## [8.1.0](https://github.com/mediacms-io/mediacms/compare/v8.0.8...v8.1.0) (2026-05-17)
### Features
* introduce x-accell headers ([9302559](https://github.com/mediacms-io/mediacms/commit/9302559d4bb3e4d0adb299ed37438b04c39e1864))
## [8.0.8](https://github.com/mediacms-io/mediacms/compare/v8.0.7...v8.0.8) (2026-05-13)
### Bug Fixes
+9
View File
@@ -202,6 +202,15 @@ THUMBNAIL_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/thumbnails/"
SUBTITLES_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/subtitles/"
HLS_DIR = os.path.join(MEDIA_ROOT, "hls/")
# Protect media files via nginx auth_request
# When True, nginx delegates authorization for /media/<protected>/... to a
# Django endpoint that checks the Media's state and the user's access.
USE_X_ACCEL_REDIRECT = True
# Subdirectories of MEDIA_ROOT that should be gated. "chunks" is intentionally
# omitted (upload state, not playback).
X_ACCEL_PROTECTED_PATHS = ["encoded", "hls", "original"]
X_ACCEL_AUTH_CACHE_SECONDS = 300
FFMPEG_COMMAND = "ffmpeg" # this is the path
FFPROBE_COMMAND = "ffprobe" # this is the path
MP4HLS = "mp4hls"
+1 -1
View File
@@ -1 +1 @@
VERSION = "8.0.7"
VERSION = "8.1.2"
+20 -2
View File
@@ -15,9 +15,27 @@ server {
location /static {
alias /home/mediacms.io/mediacms/static ;
}
location = /_media_auth {
internal;
proxy_pass http://127.0.0.1:9000/api/v1/media-auth;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Host $host;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header Cookie $http_cookie;
}
location /media/original {
alias /home/mediacms.io/mediacms/media_files/original;
location ~ ^/media/(encoded|hls|original)/(.*)$ {
auth_request /_media_auth;
alias /home/mediacms.io/mediacms/media_files/$1/$2;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
location /media {
+1
View File
@@ -106,6 +106,7 @@ urlpatterns = [
re_path(r"^api/v1/tasks$", views.TasksList.as_view()),
re_path(r"^api/v1/tasks/$", views.TasksList.as_view()),
re_path(r"^api/v1/tasks/(?P<friendly_token>[\w|\W]*)$", views.TaskDetail.as_view()),
re_path(r"^api/v1/media-auth$", views.media_auth, name="media_auth"),
re_path(r"^manage/comments$", views.manage_comments, name="manage_comments"),
re_path(r"^manage/media$", views.manage_media, name="manage_media"),
re_path(r"^manage/users$", views.manage_users, name="manage_users"),
+1
View File
@@ -10,6 +10,7 @@ from .media import MediaDetail # noqa: F401
from .media import MediaList # noqa: F401
from .media import MediaSearch # noqa: F401
from .media import media_share # noqa: F401
from .media_auth import media_auth # noqa: F401
from .pages import about # noqa: F401
from .pages import add_subtitle # noqa: F401
from .pages import approval_required # noqa: F401
+125
View File
@@ -0,0 +1,125 @@
import re
from urllib.parse import unquote
from django.conf import settings
from django.core.cache import cache
from django.db.models import Q
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_GET
from ..methods import is_mediacms_editor
from ..models import Media
UID_RE = re.compile(r"[0-9a-f]{32}")
THUMBNAILS_PREFIX = "original/thumbnails/"
def _ttl():
return getattr(settings, "X_ACCEL_AUTH_CACHE_SECONDS", 300)
def _extract_uid(uri):
if not uri:
return None
match = UID_RE.search(uri)
return match.group(0) if match else None
def _relpath_from_uri(uri):
path = unquote(uri.split("?", 1)[0])
media_url = settings.MEDIA_URL
if path.startswith(media_url):
return path[len(media_url) :]
return None
def _lookup_uid_by_path(relpath):
path_key = f"xaccel:path:{relpath}"
cached = cache.get(path_key)
if cached is not None:
return cached or None
parts = relpath.split("/", 4)
if len(parts) < 5 or parts[2] != "user":
cache.set(path_key, "", _ttl())
return None
username = parts[3]
row = Media.objects.filter(user__username=username).filter(Q(uploaded_thumbnail=relpath) | Q(uploaded_poster=relpath)).values("uid").first()
uid_hex = row["uid"].hex if row else ""
cache.set(path_key, uid_hex, _ttl())
return uid_hex or None
def _lookup_state(uid):
"""Return (state, owner_id) for a uid, or (None, None) if missing.
Cached on uid alone since state/ownership do not depend on the requester.
Uses .values() rather than .only() because Media.__init__ touches deferred
file fields, which would otherwise recurse via refresh_from_db.
"""
state_key = f"xaccel:state:{uid}"
cached = cache.get(state_key)
if cached is not None:
return cached
row = Media.objects.filter(uid=uid).values("state", "user_id").first()
value = (row["state"], row["user_id"]) if row else (None, None)
cache.set(state_key, value, _ttl())
return value
def _decide(uid, user):
state, owner_id = _lookup_state(uid)
if state is None:
return False
if state in ("public", "unlisted"):
return True
# private
if not user.is_authenticated:
return False
if owner_id == user.id:
return True
if is_mediacms_editor(user):
return True
# RBAC / MediaPermission path needs a full Media instance.
try:
media = Media.objects.get(uid=uid)
except Media.DoesNotExist:
return False
return user.has_member_access_to_media(media)
@csrf_exempt
@require_GET
def media_auth(request):
"""Authorize a protected media request from nginx auth_request.
nginx passes the original request URI in the X-Original-URI header. The
Media.uid (32 hex chars, no dashes) is embedded somewhere in that URI for
every protected path. No uid => deny. Unknown uid => deny.
"""
if not getattr(settings, "USE_X_ACCEL_REDIRECT", True):
return HttpResponse(status=204)
uri = request.META.get("HTTP_X_ORIGINAL_URI", "")
uid = _extract_uid(uri)
if not uid:
# User-uploaded thumbnails/posters don't have the uid in the filename.
# Fall back to a per-path lookup, scoped to /original/thumbnails/.
relpath = _relpath_from_uri(uri)
if relpath and relpath.startswith(THUMBNAILS_PREFIX):
uid = _lookup_uid_by_path(relpath)
if not uid:
return HttpResponse(status=403)
user = request.user
cache_key = f"xaccel:auth:{uid}:{user.id if user.is_authenticated else 'anon'}"
cached = cache.get(cache_key)
if cached is None:
allowed = _decide(uid, user)
cache.set(cache_key, allowed, _ttl())
else:
allowed = cached
return HttpResponse(status=204 if allowed else 403)
-9
View File
@@ -1,16 +1,7 @@
from django.apps import AppConfig
from .keys import ensure_keys_exist
class LtiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'lti'
verbose_name = 'LTI 1.3 Integration'
def ready(self):
"""Initialize LTI app - ensure keys exist"""
try:
ensure_keys_exist()
except Exception:
pass
-7
View File
@@ -21,10 +21,3 @@ def get_jwks():
"""
public_key = load_public_key()
return {'keys': [public_key]}
def ensure_keys_exist():
"""Ensure key pair exists in database, generate if not"""
from .models import LTIToolKeys
LTIToolKeys.get_or_create_keys()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mediacms",
"version": "8.0.8",
"version": "8.1.2",
"devDependencies": {
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
+1 -1
View File
@@ -2,4 +2,4 @@
exclude = .git,*migrations*
max-line-length = 119
#ignore=F401,F403,E501,W503
ignore=E501
ignore=E501,E203