- Check for consent before allowing users to perform activities in the administrative app.
This commit is contained in:
@@ -347,7 +347,7 @@ class ConsentVersion(db.Model):
|
|||||||
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
class ConsentStatus(Enum):
|
class ConsentStatus(str, Enum):
|
||||||
CONSENTED = 'CONSENTED'
|
CONSENTED = 'CONSENTED'
|
||||||
NOT_CONSENTED = 'NOT_CONSENTED'
|
NOT_CONSENTED = 'NOT_CONSENTED'
|
||||||
RENEWAL_REQUIRED = 'RENEWAL_REQUIRED'
|
RENEWAL_REQUIRED = 'RENEWAL_REQUIRED'
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from sqlalchemy.exc import SQLAlchemyError
|
|||||||
from common.models.entitlements import PartnerServiceLicenseTier
|
from common.models.entitlements import PartnerServiceLicenseTier
|
||||||
from common.utils.eveai_exceptions import EveAINoManagementPartnerService, EveAINoSessionPartner
|
from common.utils.eveai_exceptions import EveAINoManagementPartnerService, EveAINoSessionPartner
|
||||||
|
|
||||||
from common.utils.security_utils import current_user_has_role
|
|
||||||
|
|
||||||
|
|
||||||
class PartnerServices:
|
class PartnerServices:
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from common.utils.eveai_exceptions import EveAINoManagementPartnerService
|
|||||||
from common.utils.model_logging_utils import set_logging_information
|
from common.utils.model_logging_utils import set_logging_information
|
||||||
from datetime import datetime as dt, timezone as tz
|
from datetime import datetime as dt, timezone as tz
|
||||||
|
|
||||||
from common.utils.security_utils import current_user_has_role
|
|
||||||
|
|
||||||
|
|
||||||
class TenantServices:
|
class TenantServices:
|
||||||
@@ -201,3 +200,29 @@ class TenantServices:
|
|||||||
break
|
break
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_consent_status_details(tenant_id: int) -> Dict[str, str]:
|
||||||
|
cts = current_app.config.get("CONSENT_TYPES")
|
||||||
|
details = {}
|
||||||
|
for ct in cts:
|
||||||
|
ct = cv.consent_type
|
||||||
|
consent = (TenantConsent.query.filter_by(tenant_id=tenant_id, consent_type=ct)
|
||||||
|
.order_by(desc(TenantConsent.id))
|
||||||
|
.first())
|
||||||
|
if not consent:
|
||||||
|
details[ct] = {
|
||||||
|
'status': str(ConsentStatus.NOT_CONSENTED),
|
||||||
|
'version': str(cv.consent_version)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
if cv.consent_valid_to.date >= dt.now(tz.utc).date():
|
||||||
|
details[ct] = {
|
||||||
|
'status': str(ConsentStatus.RENEWAL_REQUIRED),
|
||||||
|
'version': str(cv.consent_version)
|
||||||
|
}
|
||||||
|
|
||||||
|
details[ct] = {
|
||||||
|
'status': str(ConsentStatus.CONSENTED),
|
||||||
|
'version': str(cv.consent_version)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from common.models.entitlements import License
|
|||||||
from common.utils.database import Database
|
from common.utils.database import Database
|
||||||
from common.utils.eveai_exceptions import EveAITenantNotFound, EveAITenantInvalid, EveAINoActiveLicense
|
from common.utils.eveai_exceptions import EveAITenantNotFound, EveAITenantInvalid, EveAINoActiveLicense
|
||||||
from datetime import datetime as dt, timezone as tz
|
from datetime import datetime as dt, timezone as tz
|
||||||
|
from common.services.user import TenantServices
|
||||||
|
|
||||||
|
|
||||||
# Definition of Trigger Handlers
|
# Definition of Trigger Handlers
|
||||||
@@ -19,12 +20,15 @@ def set_tenant_session_data(sender, user, **kwargs):
|
|||||||
# Remove partner from session if it exists
|
# Remove partner from session if it exists
|
||||||
session.pop('partner', None)
|
session.pop('partner', None)
|
||||||
|
|
||||||
|
session['consent_status'] = str(TenantServices.get_consent_status(user.tenant_id))
|
||||||
|
|
||||||
|
|
||||||
def clear_tenant_session_data(sender, user, **kwargs):
|
def clear_tenant_session_data(sender, user, **kwargs):
|
||||||
session.pop('tenant', None)
|
session.pop('tenant', None)
|
||||||
session.pop('default_language', None)
|
session.pop('default_language', None)
|
||||||
session.pop('default_llm_model', None)
|
session.pop('default_llm_model', None)
|
||||||
session.pop('partner', None)
|
session.pop('partner', None)
|
||||||
|
session.pop('consent_status', None)
|
||||||
|
|
||||||
|
|
||||||
def is_valid_tenant(tenant_id):
|
def is_valid_tenant(tenant_id):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from flask import current_app, render_template
|
from flask import current_app, render_template, request, redirect, session, flash
|
||||||
from flask_security import current_user
|
from flask_security import current_user
|
||||||
from itsdangerous import URLSafeTimedSerializer
|
from itsdangerous import URLSafeTimedSerializer
|
||||||
|
|
||||||
from common.models.user import Role
|
from common.models.user import Role, ConsentStatus
|
||||||
from common.utils.nginx_utils import prefixed_url_for
|
from common.utils.nginx_utils import prefixed_url_for
|
||||||
from common.utils.mail_utils import send_email
|
from common.utils.mail_utils import send_email
|
||||||
|
|
||||||
@@ -96,3 +96,94 @@ def current_user_roles():
|
|||||||
|
|
||||||
def all_user_roles():
|
def all_user_roles():
|
||||||
roles = [(role.id, role.name) for role in Role.query.all()]
|
roles = [(role.id, role.name) for role in Role.query.all()]
|
||||||
|
|
||||||
|
|
||||||
|
def is_exempt_endpoint(endpoint: str) -> bool:
|
||||||
|
"""Check if the endpoint is exempt from consent guard"""
|
||||||
|
if not endpoint:
|
||||||
|
return False
|
||||||
|
cfg = current_app.config or {}
|
||||||
|
endpoints_cfg = set(cfg.get('CONSENT_GUARD_EXEMPT_ENDPOINTS', []))
|
||||||
|
prefix_cfg = list(cfg.get('CONSENT_GUARD_EXEMPT_PREFIXES', []))
|
||||||
|
|
||||||
|
default_endpoints = {
|
||||||
|
'security_bp.login',
|
||||||
|
'security_bp.logout',
|
||||||
|
'security_bp.confirm_email',
|
||||||
|
'security_bp.forgot_password',
|
||||||
|
'security_bp.reset_password',
|
||||||
|
'security_bp.reset_password_request',
|
||||||
|
'user_bp.tenant_consent',
|
||||||
|
'user_bp.no_consent',
|
||||||
|
'user_bp.tenant_consent_renewal',
|
||||||
|
'user_bp.consent_renewal',
|
||||||
|
'security_bp.consent_sign',
|
||||||
|
}
|
||||||
|
default_prefixes = [
|
||||||
|
'security_bp.',
|
||||||
|
'healthz_bp.',
|
||||||
|
]
|
||||||
|
endpoints = default_endpoints.union(endpoints_cfg)
|
||||||
|
prefixes = default_prefixes + [p for p in prefix_cfg if isinstance(p, str)]
|
||||||
|
for p in prefixes:
|
||||||
|
if endpoint.startswith(p):
|
||||||
|
return True
|
||||||
|
if endpoint in endpoints:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def enforce_tenant_consent_ui():
|
||||||
|
"""Check if the user has consented to the terms of service"""
|
||||||
|
path = getattr(request, 'path', '') or ''
|
||||||
|
if path.startswith('/healthz') or path.startswith('/_healthz'):
|
||||||
|
current_app.logger.debug(f'Health check request, bypassing consent guard: {path}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
current_app.logger.debug('Not authenticated, bypassing consent guard')
|
||||||
|
return None
|
||||||
|
|
||||||
|
endpoint = request.endpoint or ''
|
||||||
|
if is_exempt_endpoint(endpoint) or request.method == 'OPTIONS':
|
||||||
|
current_app.logger.debug(f'Endpoint exempt from consent guard: {endpoint}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Global bypass: Super User and Partner Admin always allowed
|
||||||
|
if current_user.has_roles('Super User') or current_user.has_roles('Partner Admin'):
|
||||||
|
current_app.logger.debug('Global bypass: Super User or Partner Admin')
|
||||||
|
return None
|
||||||
|
|
||||||
|
tenant_id = getattr(current_user, 'tenant_id', None)
|
||||||
|
if not tenant_id:
|
||||||
|
tenant_id = session.get('tenant', {}).get('id') if session.get('tenant') else None
|
||||||
|
if not tenant_id:
|
||||||
|
return redirect(prefixed_url_for('security_bp.login', for_redirect=True))
|
||||||
|
|
||||||
|
status = session.get('consent_status', ConsentStatus.NOT_CONSENTED)
|
||||||
|
if status == ConsentStatus.CONSENTED:
|
||||||
|
current_app.logger.debug('User has consented')
|
||||||
|
return None
|
||||||
|
|
||||||
|
if status == ConsentStatus.NOT_CONSENTED:
|
||||||
|
current_app.logger.debug('User has not consented')
|
||||||
|
if current_user.has_roles('Tenant Admin'):
|
||||||
|
return redirect(prefixed_url_for('user_bp.tenant_consent', for_redirect=True))
|
||||||
|
return redirect(prefixed_url_for('user_bp.no_consent', for_redirect=True))
|
||||||
|
if status == ConsentStatus.RENEWAL_REQUIRED:
|
||||||
|
current_app.logger.debug('Consent renewal required')
|
||||||
|
if current_user.has_roles('Tenant Admin'):
|
||||||
|
flash(
|
||||||
|
"You need to renew your consent to our DPA or T&Cs. Failing to do so in time will stop you from accessing our services.",
|
||||||
|
"danger")
|
||||||
|
elif current_user.has_roles('Partner Admin'):
|
||||||
|
flash(
|
||||||
|
"Please ensure renewal of our DPA or T&Cs for the current Tenant. Failing to do so in time will stop the tenant from accessing our services.",
|
||||||
|
"danger")
|
||||||
|
else:
|
||||||
|
flash(
|
||||||
|
"Please inform your administrator or partner to renew your consent to our DPA or T&Cs. Failing to do so in time will stop you from accessing our services.",
|
||||||
|
"danger")
|
||||||
|
return None
|
||||||
|
current_app.logger.debug('Unknown consent status')
|
||||||
|
return redirect(prefixed_url_for('user_bp.no_consent', for_redirect=True))
|
||||||
|
|||||||
@@ -364,6 +364,16 @@ class Config(object):
|
|||||||
# Whether to use dynamic fallback (X-Forwarded-Prefix/Referer) when EVEAI_APP_PREFIX is empty
|
# Whether to use dynamic fallback (X-Forwarded-Prefix/Referer) when EVEAI_APP_PREFIX is empty
|
||||||
EVEAI_USE_DYNAMIC_PREFIX_FALLBACK = False
|
EVEAI_USE_DYNAMIC_PREFIX_FALLBACK = False
|
||||||
|
|
||||||
|
# Consent guard configuration (config-driven whitelist)
|
||||||
|
# List of endpoint names to exempt from the global consent guard
|
||||||
|
# Example: ['security_bp.login', 'security_bp.logout', 'user_bp.tenant_consent']
|
||||||
|
CONSENT_GUARD_EXEMPT_ENDPOINTS = []
|
||||||
|
# List of endpoint name prefixes; any endpoint starting with one of these is exempt
|
||||||
|
# Example: ['security_bp.', 'healthz_bp.']
|
||||||
|
CONSENT_GUARD_EXEMPT_PREFIXES = []
|
||||||
|
# TTL for consent status stored in session (seconds)
|
||||||
|
CONSENT_SESSION_TTL_SECONDS = int(environ.get('CONSENT_SESSION_TTL_SECONDS', '45'))
|
||||||
|
|
||||||
|
|
||||||
class DevConfig(Config):
|
class DevConfig(Config):
|
||||||
DEVELOPMENT = True
|
DEVELOPMENT = True
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from common.models.user import User, Role, Tenant, TenantDomain
|
|||||||
import common.models.interaction
|
import common.models.interaction
|
||||||
import common.models.entitlements
|
import common.models.entitlements
|
||||||
import common.models.document
|
import common.models.document
|
||||||
|
from common.utils.security_utils import enforce_tenant_consent_ui
|
||||||
from config.logging_config import configure_logging
|
from config.logging_config import configure_logging
|
||||||
from common.utils.security import set_tenant_session_data
|
from common.utils.security import set_tenant_session_data
|
||||||
from common.utils.errors import register_error_handlers
|
from common.utils.errors import register_error_handlers
|
||||||
@@ -109,6 +110,12 @@ def create_app(config_file=None):
|
|||||||
sqlalchemy_logger.setLevel(logging.DEBUG)
|
sqlalchemy_logger.setLevel(logging.DEBUG)
|
||||||
# log_request_middleware(app) # Add this when debugging nginx or another proxy
|
# log_request_middleware(app) # Add this when debugging nginx or another proxy
|
||||||
|
|
||||||
|
# Register global consent guard via extension
|
||||||
|
@app.before_request
|
||||||
|
def enforce_tenant_consent():
|
||||||
|
app.logger.debug("Enforcing tenant consent")
|
||||||
|
return enforce_tenant_consent_ui()
|
||||||
|
|
||||||
# @app.before_request
|
# @app.before_request
|
||||||
# def before_request():
|
# def before_request():
|
||||||
# # app.logger.debug(f"Before request - Session ID: {session.sid}")
|
# # app.logger.debug(f"Before request - Session ID: {session.sid}")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# views/security_views.py
|
# views/security_views.py
|
||||||
from flask import Blueprint, render_template, redirect, request, flash, current_app, abort, session
|
from flask import Blueprint, render_template, redirect, request, flash, current_app, abort, session, jsonify
|
||||||
from flask_security import current_user, login_required, login_user, logout_user
|
from flask_security import current_user, login_required, login_user, logout_user
|
||||||
from flask_security.utils import verify_and_update_password, get_message, do_flash, config_value, hash_password
|
from flask_security.utils import verify_and_update_password, get_message, do_flash, config_value, hash_password
|
||||||
from flask_security.forms import LoginForm
|
from flask_security.forms import LoginForm
|
||||||
@@ -11,7 +11,7 @@ from itsdangerous import URLSafeTimedSerializer
|
|||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from common.models.user import User, ConsentStatus
|
from common.models.user import User, ConsentStatus
|
||||||
from common.services.user import TenantServices
|
from common.services.user import TenantServices, UserServices
|
||||||
from common.utils.eveai_exceptions import EveAIException, EveAINoActiveLicense
|
from common.utils.eveai_exceptions import EveAIException, EveAINoActiveLicense
|
||||||
from common.utils.nginx_utils import prefixed_url_for
|
from common.utils.nginx_utils import prefixed_url_for
|
||||||
from eveai_app.views.security_forms import SetPasswordForm, ResetPasswordForm, ForgotPasswordForm
|
from eveai_app.views.security_forms import SetPasswordForm, ResetPasswordForm, ForgotPasswordForm
|
||||||
@@ -59,22 +59,8 @@ def login():
|
|||||||
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
|
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
|
||||||
if current_user.has_roles('Partner Admin'):
|
if current_user.has_roles('Partner Admin'):
|
||||||
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
|
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
|
||||||
consent_status = TenantServices.get_consent_status(user.tenant_id)
|
# After login, rely on global consent guard; just go to default start
|
||||||
match consent_status:
|
|
||||||
case ConsentStatus.CONSENTED:
|
|
||||||
return redirect(prefixed_url_for('user_bp.tenant_overview', for_redirect=True))
|
return redirect(prefixed_url_for('user_bp.tenant_overview', for_redirect=True))
|
||||||
case ConsentStatus.NOT_CONSENTED:
|
|
||||||
if current_user.has_roles('Tenant Admin'):
|
|
||||||
return redirect(prefixed_url_for('user_bp.tenant_consent', for_redirect=True))
|
|
||||||
else:
|
|
||||||
return redirect(prefixed_url_for('user_bp.no_consent', for_redirect=True))
|
|
||||||
case ConsentStatus.RENEWAL_REQUIRED:
|
|
||||||
if current_user.has_roles('Tenant Admin'):
|
|
||||||
return redirect(prefixed_url_for('user_bp.tenant_consent_renewal', for_redirect=True))
|
|
||||||
else:
|
|
||||||
return redirect(prefixed_url_for('user_bp.consent_renewal', for_redirect=True))
|
|
||||||
case _:
|
|
||||||
return redirect(prefixed_url_for('basic_bp.index', for_redirect=True))
|
|
||||||
else:
|
else:
|
||||||
flash('Invalid username or password', 'danger')
|
flash('Invalid username or password', 'danger')
|
||||||
current_app.logger.error(f'Invalid username or password for given email: {user.email}')
|
current_app.logger.error(f'Invalid username or password for given email: {user.email}')
|
||||||
@@ -160,6 +146,102 @@ def reset_password(token):
|
|||||||
return render_template('security/reset_password.html', reset_password_form=form)
|
return render_template('security/reset_password.html', reset_password_form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@security_bp.route('/consent/sign', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def consent_sign():
|
||||||
|
try:
|
||||||
|
# Determine tenant context
|
||||||
|
tenant_id = None
|
||||||
|
# Payload may provide a tenant_id for admins signing for others
|
||||||
|
if request.is_json:
|
||||||
|
payload = request.get_json(silent=True) or {}
|
||||||
|
tenant_id = payload.get('tenant_id')
|
||||||
|
consent_data = payload.get('consent_data', {})
|
||||||
|
else:
|
||||||
|
tenant_id = request.form.get('tenant_id')
|
||||||
|
consent_data = {}
|
||||||
|
if tenant_id is None:
|
||||||
|
# default to user's tenant (Tenant Admin)
|
||||||
|
tenant_id = current_user.tenant_id
|
||||||
|
tenant_id = int(tenant_id)
|
||||||
|
|
||||||
|
# Authorization
|
||||||
|
allowed = False
|
||||||
|
if current_user.has_roles('Super User'):
|
||||||
|
allowed = True
|
||||||
|
elif current_user.has_roles('Partner Admin') and UserServices.can_user_edit_tenant(tenant_id):
|
||||||
|
allowed = True
|
||||||
|
elif current_user.has_roles('Tenant Admin') and getattr(current_user, 'tenant_id', None) == tenant_id:
|
||||||
|
allowed = True
|
||||||
|
if not allowed:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
# Determine consent versions/types to record
|
||||||
|
cts = current_app.config.get('CONSENT_TYPES', [])
|
||||||
|
from common.models.user import TenantConsent, ConsentVersion, PartnerService
|
||||||
|
from common.services.user.partner_services import PartnerServices
|
||||||
|
|
||||||
|
# Resolve partner and management service if available in session (for Partner Admin)
|
||||||
|
partner_id = None
|
||||||
|
partner_service_id = None
|
||||||
|
try:
|
||||||
|
if 'partner' in session and session['partner'].get('services'):
|
||||||
|
partner_id = session['partner'].get('id')
|
||||||
|
mgmt = PartnerServices.get_management_service()
|
||||||
|
if mgmt:
|
||||||
|
partner_service_id = mgmt.get('id')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallbacks if not Partner Admin context
|
||||||
|
if partner_id is None:
|
||||||
|
# Try find partner by tenant (one-to-one in model)
|
||||||
|
from common.models.user import Partner, Tenant
|
||||||
|
t = Tenant.query.get(tenant_id)
|
||||||
|
if t and t.partner:
|
||||||
|
partner_id = t.partner.id
|
||||||
|
if partner_service_id is None and partner_id is not None:
|
||||||
|
ps = PartnerService.query.filter_by(partner_id=partner_id, type='MANAGEMENT_SERVICE').first()
|
||||||
|
if ps:
|
||||||
|
partner_service_id = ps.id
|
||||||
|
|
||||||
|
# For each consent type, record acceptance of latest version
|
||||||
|
now = dt.now(tz.utc)
|
||||||
|
for ct in cts:
|
||||||
|
cv = ConsentVersion.query.filter_by(consent_type=ct).order_by(ConsentVersion.consent_valid_from.desc()).first()
|
||||||
|
if not cv:
|
||||||
|
current_app.logger.error(f'No ConsentVersion found for type {ct}; skipping')
|
||||||
|
continue
|
||||||
|
tc = TenantConsent(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
partner_id=partner_id or 0,
|
||||||
|
partner_service_id=partner_service_id or 0,
|
||||||
|
user_id=current_user.id,
|
||||||
|
consent_type=ct,
|
||||||
|
consent_version=cv.consent_version,
|
||||||
|
consent_data=consent_data or {}
|
||||||
|
)
|
||||||
|
db.session.add(tc)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if request.is_json or 'application/json' in request.headers.get('Accept', ''):
|
||||||
|
return jsonify({'ok': True, 'tenant_id': tenant_id}), 200
|
||||||
|
# Default UX: go to overview
|
||||||
|
return redirect(prefixed_url_for('user_bp.tenant_overview', for_redirect=True))
|
||||||
|
except CSRFError:
|
||||||
|
if request.is_json:
|
||||||
|
return jsonify({'ok': False, 'error': 'csrf_error'}), 400
|
||||||
|
flash('Session expired. Please retry.', 'danger')
|
||||||
|
return redirect(prefixed_url_for('user_bp.no_consent', for_redirect=True))
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f'Consent signing failed: {e}')
|
||||||
|
db.session.rollback()
|
||||||
|
if request.is_json:
|
||||||
|
return jsonify({'ok': False, 'error': str(e)}), 400
|
||||||
|
flash('Failed to sign consent.', 'danger')
|
||||||
|
return redirect(prefixed_url_for('user_bp.no_consent', for_redirect=True))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -232,3 +232,5 @@ class EditConsentVersionForm(FlaskForm):
|
|||||||
consent_valid_to = DateField('Consent Valid To', id='form-control datepicker', validators=[Optional()])
|
consent_valid_to = DateField('Consent Valid To', id='form-control datepicker', validators=[Optional()])
|
||||||
|
|
||||||
|
|
||||||
|
class TenantConsentForm(FlaskForm):
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from flask_security import roles_accepted, current_user
|
|||||||
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
|
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
|
||||||
import ast
|
import ast
|
||||||
|
|
||||||
|
from wtforms import BooleanField
|
||||||
|
|
||||||
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant, TenantMake, \
|
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant, TenantMake, \
|
||||||
ConsentVersion
|
ConsentVersion
|
||||||
from common.extensions import db, security, minio_client, simple_encryption, cache_manager
|
from common.extensions import db, security, minio_client, simple_encryption, cache_manager
|
||||||
@@ -33,6 +35,32 @@ from eveai_app.views.list_views.list_view_utils import render_list_view
|
|||||||
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
|
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
|
||||||
|
|
||||||
|
|
||||||
|
# --- Consent flow placeholder views ---
|
||||||
|
@user_bp.route('/consent/tenant', methods=['GET'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def tenant_consent():
|
||||||
|
# Placeholder view; UI can be implemented in templates
|
||||||
|
return render_template('user/tenant_consent.html') if current_app.jinja_env.loader else "Tenant Consent"
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/consent/no_access', methods=['GET'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def no_consent():
|
||||||
|
return render_template('user/no_consent.html') if current_app.jinja_env.loader else "Consent required - contact your admin"
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/consent/tenant_renewal', methods=['GET'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def tenant_consent_renewal():
|
||||||
|
return render_template('user/tenant_consent_renewal.html') if current_app.jinja_env.loader else "Tenant Consent Renewal"
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/consent/renewal', methods=['GET'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def consent_renewal():
|
||||||
|
return render_template('user/consent_renewal.html') if current_app.jinja_env.loader else "Consent renewal in progress"
|
||||||
|
|
||||||
|
|
||||||
@user_bp.before_request
|
@user_bp.before_request
|
||||||
def log_before_request():
|
def log_before_request():
|
||||||
current_app.logger.debug(f'Before request: {request.path} =====================================')
|
current_app.logger.debug(f'Before request: {request.path} =====================================')
|
||||||
@@ -777,6 +805,15 @@ def edit_consent_version(consent_version_id):
|
|||||||
return render_template('user/edit_consent_version.html', form=form, consent_version_id=consent_version_id)
|
return render_template('user/edit_consent_version.html', form=form, consent_version_id=consent_version_id)
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/tenant_consent', methods=['GET', 'POST'])
|
||||||
|
@roles_accepted('Tenant Admin')
|
||||||
|
def tenant_consent():
|
||||||
|
dpa_consent = BooleanField("DPA Consent", default=False)
|
||||||
|
t_c_consent = BooleanField("T&C Consent", default=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def reset_uniquifier(user):
|
def reset_uniquifier(user):
|
||||||
security.datastore.set_uniquifier(user)
|
security.datastore.set_uniquifier(user)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""consent_version iso sepearte version for t&c and dpa
|
||||||
|
|
||||||
|
Revision ID: a6ee51d72bb4
|
||||||
|
Revises: f5f1a8b8e238
|
||||||
|
Create Date: 2025-10-14 09:00:36.680468
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a6ee51d72bb4'
|
||||||
|
down_revision = 'f5f1a8b8e238'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tenant_consent', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('consent_version', sa.String(length=20), nullable=False))
|
||||||
|
batch_op.drop_column('consent_dpa_version')
|
||||||
|
batch_op.drop_column('consent_t_c_version')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tenant_consent', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('consent_t_c_version', sa.VARCHAR(length=20), autoincrement=False, nullable=False))
|
||||||
|
batch_op.add_column(sa.Column('consent_dpa_version', sa.VARCHAR(length=20), autoincrement=False, nullable=False))
|
||||||
|
batch_op.drop_column('consent_version')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
Reference in New Issue
Block a user