7d24e7f527
Auth: - Fix local login (was redirecting to portal; now authenticates AdminUser directly) - Portal SSO still takes priority in production via nginx headers Role system (admin | editor | readonly): - New app/utils/decorators.py with editor_required and admin_required decorators - All write routes protected with editor_required (create/edit/delete/import/mask) - Settings user management protected with admin_required - Sidebar hides write-only links for readonly users - Dashboard quick actions and list page buttons hidden for readonly Settings page: - Role colour badges (admin=red, editor=blue, readonly=grey) - Inline role changer per user (dropdown auto-submit) - Reset password modal per user - Delete user button with confirmation - Add user form includes role selector with legend Portal user sync: - New /internal/sync-user endpoint receives user pre-creation from portal - INTERNAL_SYNC_SECRET added to config - portal/config.py: added internal_url for itassets app so _sync_user_to_app works
74 lines
2.4 KiB
Python
74 lines
2.4 KiB
Python
"""
|
|
Internal routes — called by the EDP portal (not by browsers).
|
|
|
|
POST /internal/sync-user
|
|
Called by the portal whenever it grants or updates a user's access to the
|
|
itassets app. Ensures the local AdminUser record exists and has the correct
|
|
role before the user ever logs in for the first time.
|
|
|
|
Body (JSON): { "username": "alice", "role": "advanced" }
|
|
Header: X-Internal-Token: <INTERNAL_SYNC_SECRET>
|
|
|
|
Portal roles are mapped to local roles:
|
|
admin → admin
|
|
advanced → editor
|
|
standard → readonly
|
|
"""
|
|
import secrets
|
|
from flask import Blueprint, request, jsonify, current_app
|
|
from app.extensions import db
|
|
from app.models.admin_user import AdminUser
|
|
from app.utils.portal_sso import _portal_role_to_local
|
|
|
|
bp = Blueprint('internal', __name__, url_prefix='/internal')
|
|
|
|
|
|
def _check_token():
|
|
expected = current_app.config.get('INTERNAL_SYNC_SECRET', '')
|
|
provided = request.headers.get('X-Internal-Token', '')
|
|
# Use hmac.compare_digest equivalent via secrets.compare_digest
|
|
if not expected or not secrets.compare_digest(expected, provided):
|
|
return False
|
|
return True
|
|
|
|
|
|
@bp.route('/sync-user', methods=['POST'])
|
|
def sync_user():
|
|
if not _check_token():
|
|
return jsonify({'error': 'Unauthorized'}), 401
|
|
|
|
data = request.get_json(silent=True) or {}
|
|
username = (data.get('username') or '').strip()
|
|
portal_role = (data.get('role') or 'standard').strip()
|
|
|
|
if not username:
|
|
return jsonify({'error': 'username is required'}), 400
|
|
|
|
local_role = _portal_role_to_local(portal_role)
|
|
|
|
try:
|
|
user = AdminUser.query.filter_by(username=username).first()
|
|
if user:
|
|
if user.role != local_role:
|
|
user.role = local_role
|
|
db.session.commit()
|
|
status = 'updated'
|
|
else:
|
|
user = AdminUser(
|
|
username=username,
|
|
email=f'{username}@portal.local',
|
|
full_name=username,
|
|
role=local_role,
|
|
is_active=True,
|
|
)
|
|
user.set_password(secrets.token_hex(32)) # random unusable password
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
status = 'created'
|
|
|
|
return jsonify({'status': status, 'username': username, 'role': local_role}), 200
|
|
|
|
except Exception as exc:
|
|
current_app.logger.error('sync-user failed: %s', exc)
|
|
return jsonify({'error': str(exc)}), 500
|