Files
enterprise_digital-platform/IT_asset_management/app/routes/internal.py
T
ske087 7d24e7f527 IT Assets: add role-based auth system and portal user sync
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
2026-07-08 21:35:16 +03:00

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