06b2152331
- Portal: add /api/internal/itassets-users endpoint (returns all portal users with itassets access + their effective role) - IT Assets: add PORTAL_INTERNAL_URL config key - IT Assets: add POST /settings/sync-from-portal route (admin only) — pulls users from the portal API and upserts them locally - Settings page: 'Sync from Portal' button next to the users table header - start-dev.sh: pass INTERNAL_SYNC_SECRET and ITASSETS_INTERNAL_URL to portal; pass PORTAL_INTERNAL_URL and INTERNAL_SYNC_SECRET to itassets - Also includes 'Back to Portal' icon button added to sidebar footer
141 lines
4.9 KiB
Python
141 lines
4.9 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)
|
|
|
|
|
|
@bp.route('/internal/itassets-users')
|
|
def itassets_users_internal():
|
|
"""
|
|
Internal endpoint for the IT Asset Management app to pull all portal users
|
|
that have itassets access, with their effective 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 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:
|
|
access = AppAccess.query.filter_by(
|
|
user_id=u.id, app_name='itassets', is_active=True
|
|
).first()
|
|
if access is None:
|
|
continue
|
|
result.append({
|
|
'username': u.username,
|
|
'email': u.email,
|
|
'portal_role': u.role,
|
|
'app_role': access.app_role or u.role,
|
|
})
|
|
return jsonify(result)
|