Files
enterprise_digital-platform/portal/app/routes/api.py
T
ske087 1f6217d347 feat: 3-tier role system across all platform apps
Portal:
- Replace is_admin boolean with role column (admin/advanced/standard)
- Settings UI: 3-tier portal role select + per-app role dropdowns
- /portal-return endpoint: re-establishes session from JWT for sub-app back-links
- /api/internal/nv-users: internal endpoint for NetworkView user sync
- portal/migrate_roles.py: one-time DB migration script
- Role badges (admin/advanced/standard) in topbar and settings table

DigiServer:
- Add editor and viewer roles (portal advanced->editor, standard->viewer)
- PlaylistPermission model: grant viewer users edit access to specific playlists
- app/utils/access.py: shared editor_required, admin_required, can_edit_playlist helpers
- Content routes: editor_required on create/delete, per-playlist permission check on mutations
- Admin: playlist_permissions route + template to manage viewer playlist grants
- Base template: hide Admin nav for viewers, Portal button (⬡) returns to portal
- content_list_new: hide create/delete for viewers; Manage vs View button per permission
- manage_playlist_content: view-only mode when user lacks edit permission

NetworkView:
- backend/src/middleware/rbac.js: requireRole + requireWriteAccess helpers
- site_permissions table: one site per advanced user
- All mutating routes guarded (admin=all, advanced=assigned site, viewer=read-only)
- Portal SSO auto-upsert: user row synced from X-Auth-Role on every request
- GET /api/users: merges portal users list with local NV data (all 4 portal users visible)
- GET/PUT /api/users/:id/site-permission: assign site to advanced user
- Settings Users tab: role badges, site dropdown for advanced, (portal only) indicator
- Sidebar: ⬡ Portal button between Settings and Logout
- Frontend build: VITE_API_BASE=/networkview/api now set in start-dev.sh

IT Assets / Server Monitor:
- portal_sso.py updated: map advanced->editor/viewer, standard->readonly
- AdminUser model: add editor role + is_editor property
2026-07-07 00:08:46 +03:00

109 lines
3.8 KiB
Python

from flask import Blueprint, request, make_response, current_app, jsonify
import jwt
import secrets as _secrets
bp = Blueprint('api', __name__, url_prefix='/api')
@bp.route('/verify-token')
def verify_token():
"""
Called internally by nginx auth_request.
Reads the platform JWT cookie, verifies it, and returns 200 with user
identity headers on success, or 401/403 on failure.
"""
token = request.cookies.get(current_app.config['PORTAL_COOKIE_NAME'])
if not token:
return '', 401
try:
payload = jwt.decode(
token,
current_app.config['PORTAL_JWT_SECRET'],
algorithms=['HS256'],
options={'require': ['exp', 'sub', 'user_id']},
)
except jwt.ExpiredSignatureError:
return '', 401
except jwt.InvalidTokenError:
return '', 401
# Per-app access check based on the original request URI
original_uri = request.headers.get('X-Original-URI', '')
required_app = _app_from_uri(original_uri)
user_id = payload.get('user_id')
portal_role = payload.get('role', 'user')
if required_app:
user_apps = payload.get('apps', [])
if required_app not in user_apps:
return '', 403
# Resolve the effective role for this specific app.
# If the admin has set a per-app role override in AppAccess, use that;
# otherwise fall back to the portal-level role from the JWT.
effective_role = portal_role
if required_app and user_id:
try:
from app.models.app_access import AppAccess
access = AppAccess.query.filter_by(
user_id=user_id, app_name=required_app, is_active=True
).first()
if access and access.app_role:
effective_role = access.app_role
except Exception:
pass # fall back to portal role
resp = make_response('', 200)
resp.headers['X-Auth-User-Id'] = str(user_id or '')
resp.headers['X-Auth-Username'] = payload.get('sub', '')
resp.headers['X-Auth-Role'] = effective_role
return resp
def _app_from_uri(uri):
if uri.startswith('/digiserver/'):
return 'digiserver'
if uri.startswith('/itassets/'):
return 'itassets'
if uri.startswith('/networkview/'):
return 'networkview'
if uri.startswith('/srvmonitor/'):
return 'srvmonitor'
return None
@bp.route('/internal/nv-users')
def nv_users_internal():
"""
Internal endpoint for the NetworkView backend to fetch all portal users
that have NetworkView access, together with their assigned NV role.
Protected by X-Internal-Token header (shared INTERNAL_SYNC_SECRET).
"""
secret = current_app.config.get('INTERNAL_SYNC_SECRET', '')
provided = request.headers.get('X-Internal-Token', '')
if not secret or not _secrets.compare_digest(provided, provided and secret):
# constant-time compare — reject if secret mismatch or empty
if not secret or not provided or not _secrets.compare_digest(provided, secret):
return jsonify({'error': 'forbidden'}), 403
from app.models.user import PortalUser
from app.models.app_access import AppAccess
users = PortalUser.query.filter_by(is_active=True).order_by(PortalUser.username).all()
result = []
for u in users:
nv_access = AppAccess.query.filter_by(
user_id=u.id, app_name='networkview', is_active=True
).first()
if nv_access is None:
continue # skip users who have no NV access at all
result.append({
'portal_id': str(u.id),
'username': u.username,
'email': u.email,
'portal_role': u.role,
'nv_role': nv_access.app_role or u.role, # per-app override or portal role
})
return jsonify(result)